blob: 249b4b76b36c10068280262de96f53e1dbc988a4 [file] [log] [blame]
[email protected]405b87e2015-11-12 18:08:341#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 13:17:552# Copyright (c) 2013 The Chromium Authors. All rights reserved.
[email protected]725f1c32011-04-01 20:24:543# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
[email protected]cc51cd02010-12-23 00:48:396# Copyright (C) 2008 Evan Martin <[email protected]>
7
Andrii Shyshkalov03da1502018-10-15 03:42:348"""A git-command for integrating reviews on Gerrit."""
[email protected]725f1c32011-04-01 20:24:549
vapiera7fbd5a2016-06-16 16:17:4910from __future__ import print_function
11
[email protected]6a0b07c2013-07-10 01:29:1912from distutils.version import LooseVersion
[email protected]ffde55c2015-03-12 00:44:1713from multiprocessing.pool import ThreadPool
[email protected]3421c992014-11-02 02:20:3214import base64
[email protected]2dd99862015-06-22 12:22:1815import collections
Andrii Shyshkalovf3a20ae2017-01-24 20:23:5716import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 15:05:4917import datetime
Andrii Shyshkalovcd6a9362016-12-07 11:04:1218import fnmatch
[email protected]6ebaf782015-05-12 19:17:5419import httplib
Andrii Shyshkalov34924cd2017-03-15 16:08:3220import itertools
[email protected]4f6852c2012-04-20 20:39:2021import json
[email protected]cc51cd02010-12-23 00:48:3922import logging
[email protected]cf197482016-04-29 20:15:5323import multiprocessing
[email protected]cc51cd02010-12-23 00:48:3924import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 15:52:1827import shutil
[email protected]78c4b982012-02-14 02:20:2628import stat
[email protected]cc51cd02010-12-23 00:48:3929import sys
Aaron Gable9a03ae02017-11-03 18:31:0730import tempfile
[email protected]cc51cd02010-12-23 00:48:3931import textwrap
[email protected]b015fac2016-02-26 14:52:0132import urllib
[email protected]cc51cd02010-12-23 00:48:3933import urllib2
[email protected]967c0a82013-06-17 22:52:2434import urlparse
[email protected]b015fac2016-02-26 14:52:0135import uuid
[email protected]00858c82013-12-02 23:08:0336import webbrowser
[email protected]3421c992014-11-02 02:20:3237import zlib
[email protected]cc51cd02010-12-23 00:48:3938
39try:
Quinten Yearsleyb2cc4a92016-12-15 21:53:2640 import readline # pylint: disable=import-error,W0611
[email protected]cc51cd02010-12-23 00:48:3941except ImportError:
42 pass
43
[email protected]2e23ce32013-05-07 12:42:2844from third_party import colorama
[email protected]6ebaf782015-05-12 19:17:5445from third_party import httplib2
[email protected]2a74d372011-03-29 19:05:5046from third_party import upload
[email protected]cf6a5d22015-04-09 22:02:0047import auth
skobes6468b902016-10-24 15:45:1048import checkout
[email protected]3ac1c4e2014-01-16 02:44:4249import clang_format
[email protected]e0a7c5d2015-02-23 20:30:0850import dart_format
[email protected]596cd5c2016-04-04 21:34:3951import setup_color
[email protected]6f09cd92011-04-01 16:38:1252import fix_encoding
[email protected]0e0436a2011-10-25 13:32:4153import gclient_utils
[email protected]aa5ced12016-03-29 09:41:1454import gerrit_util
[email protected]151ebcf2016-03-09 01:08:2555import git_cache
[email protected]9e849272014-04-04 00:31:5556import git_common
[email protected]09d7a6a2016-03-04 15:44:4857import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:0258import metrics
[email protected]336f9122014-09-04 02:16:5559import owners
[email protected]9e849272014-04-04 00:31:5560import owners_finder
[email protected]2a74d372011-03-29 19:05:5061import presubmit_support
[email protected]cab38e92011-04-09 00:30:5162import rietveld
[email protected]2a74d372011-03-29 19:05:5063import scm
Francois Dorayd42c6812017-05-30 19:10:2064import split_cl
[email protected]0633fb42013-08-16 20:06:1465import subcommand
[email protected]32f9f5e2011-09-14 13:41:4766import subprocess2
[email protected]2a74d372011-03-29 19:05:5067import watchlists
68
tandrii7400cf02016-06-21 15:48:0769__version__ = '2.0'
[email protected]2a74d372011-03-29 19:05:5070
tandrii9d2c7a32016-06-22 10:42:4571COMMIT_BOT_EMAIL = '[email protected]'
iannuccie7f68952016-08-16 00:45:2972DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 18:08:1473POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
[email protected]cc51cd02010-12-23 00:48:3974DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
[email protected]c68112d2015-03-03 12:48:0675REFS_THAT_ALIAS_TO_OTHER_REFS = {
76 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
77 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
78}
[email protected]cc51cd02010-12-23 00:48:3979
[email protected]44202a22014-03-11 19:22:1880# 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:4284# File name for yapf style config files.
85YAPF_CONFIG_FILENAME = '.style.yapf'
86
borenet6c0efe62016-10-19 15:13:2987# Buildbucket master name prefix.
88MASTER_PREFIX = 'master.'
89
Edward Lemur83bd7f42018-10-10 00:14:2190# TODO(crbug.com/881860): Remove
91# Log gerrit failures to a gerrit_util.GERRIT_ERR_LOG_FILE.
92GERRIT_ERR_LOGGER = logging.getLogger('GerritErrorLogs')
93
[email protected]2e23ce32013-05-07 12:42:2894# Shortcut since it quickly becomes redundant.
95Fore = colorama.Fore
[email protected]90541732011-04-01 17:54:1896
[email protected]ddd59412011-11-30 14:20:3897# Initialized in main()
98settings = None
99
Andrii Shyshkalov768f1d82016-12-08 14:10:13100# 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 20:39:42103# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 14:10:13104_IS_BEING_TESTED = False
105
[email protected]ddd59412011-11-30 14:20:38106
Christopher Lamf732cd52017-01-24 01:40:11107def DieWithError(message, change_desc=None):
108 if change_desc:
109 SaveDescriptionBackup(change_desc)
110
vapiera7fbd5a2016-06-16 16:17:49111 print(message, file=sys.stderr)
[email protected]cc51cd02010-12-23 00:48:39112 sys.exit(1)
113
114
Christopher Lamf732cd52017-01-24 01:40:11115def SaveDescriptionBackup(change_desc):
116 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09117 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 01:40:11118 backup_file = open(backup_path, 'w')
119 backup_file.write(change_desc.description)
120 backup_file.close()
121
122
[email protected]8b0553c2014-02-11 00:33:37123def 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
[email protected]566a02a2014-08-22 01:34:13129
[email protected]627d9002016-04-29 00:00:52130def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
[email protected]cc51cd02010-12-23 00:48:39131 try:
[email protected]627d9002016-04-29 00:00:52132 return subprocess2.check_output(args, shell=shell, **kwargs)
[email protected]78936cb2013-04-11 00:17:52133 except subprocess2.CalledProcessError as e:
134 logging.debug('Failed running %s', args)
[email protected]32f9f5e2011-09-14 13:41:47135 if not error_ok:
[email protected]cc51cd02010-12-23 00:48:39136 DieWithError(
[email protected]32f9f5e2011-09-14 13:41:47137 'Command "%s" failed.\n%s' % (
138 ' '.join(args), error_message or e.stdout or ''))
139 return e.stdout
[email protected]cc51cd02010-12-23 00:48:39140
141
142def RunGit(args, **kwargs):
[email protected]32f9f5e2011-09-14 13:41:47143 """Returns stdout."""
[email protected]82b91cd2013-07-09 06:33:41144 return RunCommand(['git'] + args, **kwargs)
[email protected]cc51cd02010-12-23 00:48:39145
146
[email protected]3b7e15c2014-01-21 17:44:47147def RunGitWithCode(args, suppress_stderr=False):
[email protected]32f9f5e2011-09-14 13:41:47148 """Returns return code and stdout."""
tandrii5d48c322016-08-18 23:19:37149 if suppress_stderr:
150 stderr = subprocess2.VOID
151 else:
152 stderr = sys.stderr
[email protected]9bb85e22012-06-13 20:28:23153 try:
tandrii5d48c322016-08-18 23:19:37154 (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 06:33:34160 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 23:19:37161 return e.returncode, e.stdout
[email protected]cc51cd02010-12-23 00:48:39162
163
[email protected]27386dd2015-02-16 10:45:39164def RunGitSilent(args):
[email protected]cbd7dc32016-05-31 10:33:50165 """Returns stdout, suppresses stderr and ignores the return code."""
[email protected]27386dd2015-02-16 10:45:39166 return RunGitWithCode(args, suppress_stderr=True)[1]
167
168
[email protected]6a0b07c2013-07-10 01:29:19169def IsGitVersionAtLeast(min_version):
[email protected]cc56ee42013-07-10 22:16:29170 prefix = 'git version '
[email protected]6a0b07c2013-07-10 01:29:19171 version = RunGit(['--version']).strip()
[email protected]cc56ee42013-07-10 22:16:29172 return (version.startswith(prefix) and
173 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
[email protected]6a0b07c2013-07-10 01:29:19174
175
[email protected]8ba38ff2015-06-11 21:41:25176def 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 14:09:44183def 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
[email protected]90541732011-04-01 17:54:18190def 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 13:49:38198def 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 15:52:18202 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 13:49:38203 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 23:19:37222def _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
[email protected]caa16552013-03-18 20:45:05238 if not branch:
tandrii5d48c322016-08-18 23:19:37239 return default
[email protected]caa16552013-03-18 20:45:05240
tandrii5d48c322016-08-18 23:19:37241 args = ['config']
tandrii33a46ff2016-08-23 12:53:40242 if value_type == bool:
tandrii5d48c322016-08-18 23:19:37243 args.append('--bool')
tandrii33a46ff2016-08-23 12:53:40244 # 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 23:19:37246 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
[email protected]79540052012-10-19 23:15:26255 return default
256
257
tandrii5d48c322016-08-18 23:19:37258def _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 16:18:40268 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 23:19:37269 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 23:19:37274 else:
tandrii33a46ff2016-08-23 12:53:40275 # 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 23:19:37277 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 16:47:09284def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 20:39:42285 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 16:47:09286
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
[email protected]45453142015-09-15 08:45:22304def _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
[email protected]6ebaf782015-05-12 19:17:54314def _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 15:13:29322 if master.startswith(MASTER_PREFIX):
[email protected]6ebaf782015-05-12 19:17:54323 return master
borenet6c0efe62016-10-19 15:13:29324 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
[email protected]6ebaf782015-05-12 19:17:54338
339
[email protected]b015fac2016-02-26 14:52:01340def _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'):
[email protected]baff4e12016-03-08 00:33:57352 error = content_json.get('error')
353 if error.get('code') == 403:
354 raise BuildbucketResponseException(
355 'Access denied: %s' % error.get('message', ''))
[email protected]b015fac2016-02-26 14:52:01356 msg = 'Error in response. Reason: %s. Message: %s.' % (
[email protected]baff4e12016-03-08 00:33:57357 error.get('reason', ''), error.get('message', ''))
[email protected]b015fac2016-02-26 14:52:01358 raise BuildbucketResponseException(msg)
359
360 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 19:37:32361 if content_json is None:
[email protected]b015fac2016-02-26 14:52:01362 raise BuildbucketResponseException(
363 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 19:34:14364 'Please file bugs at http://crbug.com, '
365 'component "Infra>Platform>BuildBucket".' %
[email protected]b015fac2016-02-26 14:52:01366 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 14:09:44373 time_sleep(0.5 + 1.5*try_count)
[email protected]b015fac2016-02-26 14:52:01374 try_count += 1
375 assert False, 'unreachable'
376
377
qyearsley1fdfcb62016-10-24 20:22:03378def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 18:57:22379 """Returns a dict mapping bucket names to builders and tests,
380 for triggering try jobs.
qyearsley1fdfcb62016-10-24 20:22:03381 """
qyearsleydd49f942016-10-28 18:57:22382 # 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 20:22:03384 if not options.bot:
385 change = changelist.GetChange(
386 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 16:02:26387 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 23:23:30388 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 20:22:03389 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 23:23:30396 if masters is None:
397 return None
Sergiy Byelozyorov935b93f2016-11-28 19:41:56398 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 20:22:03399
qyearsley1fdfcb62016-10-24 20:22:03400 if options.bucket:
401 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 18:57:22402 if options.master:
403 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 20:22:03404
qyearsleydd49f942016-10-28 18:57:22405 # 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 20:22:03414
415
qyearsley123a4682016-10-26 16:12:17416def _get_bucket_map_for_builders(builders):
417 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 20:22:03418 map_url = 'https://builders-map.appspot.com/'
419 try:
qyearsley123a4682016-10-26 16:12:17420 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 20:22:03421 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 16:12:17426 if not builders_map:
qyearsley1fdfcb62016-10-24 20:22:03427 return None, 'Failed to build master map.'
428
qyearsley123a4682016-10-26 16:12:17429 bucket_map = {}
430 for builder in builders:
Nodir Turakulovb422e682018-02-21 06:51:30431 bucket = builders_map.get(builder, {}).get('bucket')
432 if bucket:
433 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 16:12:17434 return bucket_map, None
qyearsley1fdfcb62016-10-24 20:22:03435
436
Andrii Shyshkalovf9648b52018-02-22 06:32:42437def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 20:22:03438 """Sends a request to Buildbucket to trigger try jobs for a changelist.
439
440 Args:
Aaron Gablefb28d482018-04-02 20:08:06441 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 20:22:03442 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 13:02:30446 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 20:08:06453 # Cache the buildbucket credentials under the codereview host key, so that
454 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 13:02:30455 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
[email protected]6ebaf782015-05-12 19:17:54456 http = authenticator.authorize(httplib2.Http())
457 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 13:02:30458
[email protected]6ebaf782015-05-12 19:17:54459 buildbucket_put_url = (
460 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
[email protected]db375572015-08-17 19:22:23461 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34462 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 13:02:30463 hostname=codereview_host,
464 issue=changelist.GetIssue(),
[email protected]6ebaf782015-05-12 19:17:54465 patch=patchset)
tandrii8c5a3532016-11-04 14:52:02466
Quinten Yearsley0c62da92017-05-31 20:39:42467 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-22 06:32:42468 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 14:52:02469 if options.clobber:
470 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 13:02:30471 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 14:52:02472 if extra_properties:
473 shared_parameters_properties.update(extra_properties)
[email protected]6ebaf782015-05-12 19:17:54474
475 batch_req_body = {'builds': []}
476 print_text = []
477 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 15:13:29478 for bucket, builders_and_tests in sorted(buckets.iteritems()):
479 print_text.append('Bucket: %s' % bucket)
480 master = None
481 if bucket.startswith(MASTER_PREFIX):
482 master = _unprefix_master(bucket)
[email protected]6ebaf782015-05-12 19:17:54483 for builder, tests in sorted(builders_and_tests.iteritems()):
484 print_text.append(' %s: %s' % (builder, tests))
485 parameters = {
486 'builder_name': builder,
[email protected]d2217312015-09-21 15:51:21487 'changes': [{
Andrii Shyshkaloveadad922017-01-26 08:38:30488 'author': {'email': changelist.GetIssueOwner()},
[email protected]d2217312015-09-21 15:51:21489 'revision': options.revision,
490 }],
tandrii8c5a3532016-11-04 14:52:02491 'properties': shared_parameters_properties.copy(),
[email protected]6ebaf782015-05-12 19:17:54492 }
[email protected]2403e802016-04-29 12:34:42493 if 'presubmit' in builder.lower():
494 parameters['properties']['dry_run'] = 'true'
[email protected]3764fa22015-10-21 16:40:40495 if tests:
496 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 15:13:29497
498 tags = [
499 'builder:%s' % builder,
500 'buildset:%s' % buildset,
501 'user_agent:git_cl_try',
502 ]
503 if master:
504 parameters['properties']['master'] = master
505 tags.append('master:%s' % master)
506
[email protected]6ebaf782015-05-12 19:17:54507 batch_req_body['builds'].append(
508 {
509 'bucket': bucket,
510 'parameters_json': json.dumps(parameters),
[email protected]b015fac2016-02-26 14:52:01511 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 15:13:29512 'tags': tags,
[email protected]6ebaf782015-05-12 19:17:54513 }
514 )
515
[email protected]b015fac2016-02-26 14:52:01516 _buildbucket_retry(
qyearsleyeab3c042016-08-24 16:18:28517 'triggering try jobs',
[email protected]b015fac2016-02-26 14:52:01518 http,
519 buildbucket_put_url,
520 'PUT',
521 body=json.dumps(batch_req_body),
522 headers={'Content-Type': 'application/json'}
523 )
[email protected]35c61452016-02-26 15:24:57524 print_text.append('To see results here, run: git cl try-results')
525 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 16:17:49526 print('\n'.join(print_text))
[email protected]44424542015-06-02 18:35:29527
[email protected]6ebaf782015-05-12 19:17:54528
tandrii221ab252016-10-06 15:12:04529def fetch_try_jobs(auth_config, changelist, buildbucket_host,
530 patchset=None):
qyearsleyeab3c042016-08-24 16:18:28531 """Fetches try jobs from buildbucket.
[email protected]b015fac2016-02-26 14:52:01532
qyearsley53f48a12016-09-01 17:45:13533 Returns a map from build id to build info as a dictionary.
[email protected]b015fac2016-02-26 14:52:01534 """
tandrii221ab252016-10-06 15:12:04535 assert buildbucket_host
536 assert changelist.GetIssue(), 'CL must be uploaded first'
537 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
538 patchset = patchset or changelist.GetMostRecentPatchset()
539 assert patchset, 'CL must be uploaded first'
540
541 codereview_url = changelist.GetCodereviewServer()
542 codereview_host = urlparse.urlparse(codereview_url).hostname
543 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
[email protected]b015fac2016-02-26 14:52:01544 if authenticator.has_cached_credentials():
545 http = authenticator.authorize(httplib2.Http())
546 else:
vapiera7fbd5a2016-06-16 16:17:49547 print('Warning: Some results might be missing because %s' %
548 # Get the message on how to login.
tandrii221ab252016-10-06 15:12:04549 (auth.LoginRequiredError(codereview_host).message,))
[email protected]b015fac2016-02-26 14:52:01550 http = httplib2.Http()
551
552 http.force_exception_to_status_code = True
553
Andrii Shyshkalov03da1502018-10-15 03:42:34554 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 15:12:04555 hostname=codereview_host,
[email protected]b015fac2016-02-26 14:52:01556 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 15:12:04557 patch=patchset)
[email protected]b015fac2016-02-26 14:52:01558 params = {'tag': 'buildset:%s' % buildset}
559
560 builds = {}
561 while True:
562 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 15:12:04563 hostname=buildbucket_host,
[email protected]b015fac2016-02-26 14:52:01564 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 16:18:28565 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
[email protected]b015fac2016-02-26 14:52:01566 for build in content.get('builds', []):
567 builds[build['id']] = build
568 if 'next_cursor' in content:
569 params['start_cursor'] = content['next_cursor']
570 else:
571 break
572 return builds
573
574
qyearsleyeab3c042016-08-24 16:18:28575def print_try_jobs(options, builds):
[email protected]b015fac2016-02-26 14:52:01576 """Prints nicely result of fetch_try_jobs."""
577 if not builds:
Quinten Yearsley0c62da92017-05-31 20:39:42578 print('No try jobs scheduled.')
[email protected]b015fac2016-02-26 14:52:01579 return
580
581 # Make a copy, because we'll be modifying builds dictionary.
582 builds = builds.copy()
583 builder_names_cache = {}
584
585 def get_builder(b):
586 try:
587 return builder_names_cache[b['id']]
588 except KeyError:
589 try:
590 parameters = json.loads(b['parameters_json'])
591 name = parameters['builder_name']
592 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 20:39:42593 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 16:17:49594 b['id'], error))
[email protected]b015fac2016-02-26 14:52:01595 name = None
596 builder_names_cache[b['id']] = name
597 return name
598
599 def get_bucket(b):
600 bucket = b['bucket']
601 if bucket.startswith('master.'):
602 return bucket[len('master.'):]
603 return bucket
604
605 if options.print_master:
606 name_fmt = '%%-%ds %%-%ds' % (
607 max(len(str(get_bucket(b))) for b in builds.itervalues()),
608 max(len(str(get_builder(b))) for b in builds.itervalues()))
609 def get_name(b):
610 return name_fmt % (get_bucket(b), get_builder(b))
611 else:
612 name_fmt = '%%-%ds' % (
613 max(len(str(get_builder(b))) for b in builds.itervalues()))
614 def get_name(b):
615 return name_fmt % get_builder(b)
616
617 def sort_key(b):
618 return b['status'], b.get('result'), get_name(b), b.get('url')
619
620 def pop(title, f, color=None, **kwargs):
621 """Pop matching builds from `builds` dict and print them."""
622
[email protected]6cf98c82016-03-15 11:56:00623 if not options.color or color is None:
[email protected]b015fac2016-02-26 14:52:01624 colorize = str
625 else:
626 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
627
628 result = []
629 for b in builds.values():
630 if all(b.get(k) == v for k, v in kwargs.iteritems()):
631 builds.pop(b['id'])
632 result.append(b)
633 if result:
vapiera7fbd5a2016-06-16 16:17:49634 print(colorize(title))
[email protected]b015fac2016-02-26 14:52:01635 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 16:17:49636 print(' ', colorize('\t'.join(map(str, f(b)))))
[email protected]b015fac2016-02-26 14:52:01637
638 total = len(builds)
639 pop(status='COMPLETED', result='SUCCESS',
640 title='Successes:', color=Fore.GREEN,
641 f=lambda b: (get_name(b), b.get('url')))
642 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
643 title='Infra Failures:', color=Fore.MAGENTA,
644 f=lambda b: (get_name(b), b.get('url')))
645 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
646 title='Failures:', color=Fore.RED,
647 f=lambda b: (get_name(b), b.get('url')))
648 pop(status='COMPLETED', result='CANCELED',
649 title='Canceled:', color=Fore.MAGENTA,
650 f=lambda b: (get_name(b),))
651 pop(status='COMPLETED', result='FAILURE',
652 failure_reason='INVALID_BUILD_DEFINITION',
653 title='Wrong master/builder name:', color=Fore.MAGENTA,
654 f=lambda b: (get_name(b),))
655 pop(status='COMPLETED', result='FAILURE',
656 title='Other failures:',
657 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
658 pop(status='COMPLETED',
659 title='Other finished:',
660 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
661 pop(status='STARTED',
662 title='Started:', color=Fore.YELLOW,
663 f=lambda b: (get_name(b), b.get('url')))
664 pop(status='SCHEDULED',
665 title='Scheduled:',
666 f=lambda b: (get_name(b), 'id=%s' % b['id']))
667 # The last section is just in case buildbucket API changes OR there is a bug.
668 pop(title='Other:',
669 f=lambda b: (get_name(b), 'id=%s' % b['id']))
670 assert len(builds) == 0
qyearsleyeab3c042016-08-24 16:18:28671 print('Total: %d try jobs' % total)
[email protected]b015fac2016-02-26 14:52:01672
673
Aiden Bennerc08566e2018-10-03 17:52:42674def _ComputeDiffLineRanges(files, upstream_commit):
675 """Gets the changed line ranges for each file since upstream_commit.
676
677 Parses a git diff on provided files and returns a dict that maps a file name
678 to an ordered list of range tuples in the form (start_line, count).
679 Ranges are in the same format as a git diff.
680 """
681 # If files is empty then diff_output will be a full diff.
682 if len(files) == 0:
683 return {}
684
685 # Take diff and find the line ranges where there are changes.
686 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
687 diff_output = RunGit(diff_cmd)
688
689 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
690 # 2 capture groups
691 # 0 == fname of diff file
692 # 1 == 'diff_start,diff_count' or 'diff_start'
693 # will match each of
694 # diff --git a/foo.foo b/foo.py
695 # @@ -12,2 +14,3 @@
696 # @@ -12,2 +17 @@
697 # running re.findall on the above string with pattern will give
698 # [('foo.py', ''), ('', '14,3'), ('', '17')]
699
700 curr_file = None
701 line_diffs = {}
702 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
703 if match[0] != '':
704 # Will match the second filename in diff --git a/a.py b/b.py.
705 curr_file = match[0]
706 line_diffs[curr_file] = []
707 else:
708 # Matches +14,3
709 if ',' in match[1]:
710 diff_start, diff_count = match[1].split(',')
711 else:
712 # Single line changes are of the form +12 instead of +12,1.
713 diff_start = match[1]
714 diff_count = 1
715
716 diff_start = int(diff_start)
717 diff_count = int(diff_count)
718
719 # If diff_count == 0 this is a removal we can ignore.
720 line_diffs[curr_file].append((diff_start, diff_count))
721
722 return line_diffs
723
724
725def _FindYapfConfigFile(fpath,
726 yapf_config_cache,
727 top_dir=None,
728 default_style=None):
729 """Checks if a yapf file is in any parent directory of fpath until top_dir.
730
731 Recursively checks parent directories to find yapf file
732 and if no yapf file is found returns default_style.
733 Uses yapf_config_cache as a cache for previously found files.
734 """
735 # Return result if we've already computed it.
736 if fpath in yapf_config_cache:
737 return yapf_config_cache[fpath]
738
739 # Check if there is a style file in the current directory.
740 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
741 dirname = os.path.dirname(fpath)
742 if os.path.isfile(yapf_file):
743 ret = yapf_file
744 elif fpath == top_dir or dirname == fpath:
745 # If we're at the top level directory, or if we're at root
746 # use the chromium default yapf style.
747 ret = default_style
748 else:
749 # Otherwise recurse on the current directory.
750 ret = _FindYapfConfigFile(dirname, yapf_config_cache, top_dir,
751 default_style)
752 yapf_config_cache[fpath] = ret
753 return ret
754
755
qyearsley53f48a12016-09-01 17:45:13756def write_try_results_json(output_file, builds):
757 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
758
759 The input |builds| dict is assumed to be generated by Buildbucket.
760 Buildbucket documentation: http://goo.gl/G0s101
761 """
762
763 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-12 00:36:54764 """Extracts some of the information from one build dict."""
765 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 17:45:13766 return {
767 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 17:45:13768 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-12 00:36:54769 'builder_name': parameters.get('builder_name'),
770 'created_ts': build.get('created_ts'),
771 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 17:45:13772 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-12 00:36:54773 'result': build.get('result'),
774 'status': build.get('status'),
775 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 17:45:13776 'url': build.get('url'),
777 }
778
779 converted = []
780 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42781 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 17:45:13782 write_json(output_file, converted)
783
784
Aaron Gable13101a62018-02-09 21:20:41785def print_stats(args):
[email protected]49e3d802012-07-18 23:54:45786 """Prints statistics about the change to the user."""
787 # --no-ext-diff is broken in some versions of Git, so try to work around
788 # this by overriding the environment (but there is still a problem if the
789 # git config key "diff.external" is used).
[email protected]8b0553c2014-02-11 00:33:37790 env = GetNoGitPagerEnv()
[email protected]49e3d802012-07-18 23:54:45791 if 'GIT_EXTERNAL_DIFF' in env:
792 del env['GIT_EXTERNAL_DIFF']
[email protected]79540052012-10-19 23:15:26793
[email protected]d057f9a2014-05-29 21:09:36794 try:
795 stdout = sys.stdout.fileno()
796 except AttributeError:
797 stdout = None
[email protected]49e3d802012-07-18 23:54:45798 return subprocess2.call(
Aaron Gable13101a62018-02-09 21:20:41799 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
[email protected]d057f9a2014-05-29 21:09:36800 stdout=stdout, env=env)
[email protected]49e3d802012-07-18 23:54:45801
802
[email protected]6ebaf782015-05-12 19:17:54803class BuildbucketResponseException(Exception):
804 pass
805
806
[email protected]cc51cd02010-12-23 00:48:39807class Settings(object):
808 def __init__(self):
809 self.default_server = None
810 self.cc = None
[email protected]7a54e812014-02-11 19:57:22811 self.root = None
[email protected]cc51cd02010-12-23 00:48:39812 self.tree_status_url = None
813 self.viewvc_url = None
814 self.updated = False
[email protected]e8077812012-02-03 03:41:46815 self.is_gerrit = None
[email protected]54b400c2016-01-14 10:08:25816 self.squash_gerrit_uploads = None
[email protected]28253532016-04-14 13:46:56817 self.gerrit_skip_ensure_authenticated = None
[email protected]615a2622013-05-03 13:20:14818 self.git_editor = None
[email protected]152cf832014-06-11 21:37:49819 self.project = None
[email protected]6abc6522014-12-02 07:34:49820 self.force_https_commit_url = None
[email protected]cc51cd02010-12-23 00:48:39821
822 def LazyUpdateIfNeeded(self):
823 """Updates the settings from a codereview.settings file, if available."""
824 if not self.updated:
[email protected]87884cc2014-01-03 22:23:41825 # The only value that actually changes the behavior is
826 # autoupdate = "false". Everything else means "true".
[email protected]3ac1c4e2014-01-16 02:44:42827 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
[email protected]87884cc2014-01-03 22:23:41828 error_ok=True
829 ).strip().lower()
830
[email protected]cc51cd02010-12-23 00:48:39831 cr_settings_file = FindCodereviewSettingsFile()
[email protected]87884cc2014-01-03 22:23:41832 if autoupdate != 'false' and cr_settings_file:
[email protected]cc51cd02010-12-23 00:48:39833 LoadCodereviewSettingsFromFile(cr_settings_file)
834 self.updated = True
835
836 def GetDefaultServerUrl(self, error_ok=False):
837 if not self.default_server:
838 self.LazyUpdateIfNeeded()
[email protected]eb5edbc2012-01-16 17:03:28839 self.default_server = gclient_utils.UpgradeToHttps(
[email protected]8b0553c2014-02-11 00:33:37840 self._GetRietveldConfig('server', error_ok=True))
[email protected]cc51cd02010-12-23 00:48:39841 if error_ok:
842 return self.default_server
843 if not self.default_server:
844 error_message = ('Could not find settings file. You must configure '
845 'your review setup by running "git cl config".')
[email protected]eb5edbc2012-01-16 17:03:28846 self.default_server = gclient_utils.UpgradeToHttps(
[email protected]8b0553c2014-02-11 00:33:37847 self._GetRietveldConfig('server', error_message=error_message))
[email protected]cc51cd02010-12-23 00:48:39848 return self.default_server
849
[email protected]7a54e812014-02-11 19:57:22850 @staticmethod
851 def GetRelativeRoot():
852 return RunGit(['rev-parse', '--show-cdup']).strip()
[email protected]8b0553c2014-02-11 00:33:37853
[email protected]cc51cd02010-12-23 00:48:39854 def GetRoot(self):
[email protected]7a54e812014-02-11 19:57:22855 if self.root is None:
856 self.root = os.path.abspath(self.GetRelativeRoot())
857 return self.root
[email protected]cc51cd02010-12-23 00:48:39858
[email protected]151ebcf2016-03-09 01:08:25859 def GetGitMirror(self, remote='origin'):
860 """If this checkout is from a local git mirror, return a Mirror object."""
[email protected]81593742016-03-09 20:27:58861 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
[email protected]151ebcf2016-03-09 01:08:25862 if not os.path.isdir(local_url):
863 return None
864 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
865 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 20:23:57866 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 15:44:13867 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
[email protected]151ebcf2016-03-09 01:08:25868 if mirror.exists():
869 return mirror
870 return None
871
[email protected]cc51cd02010-12-23 00:48:39872 def GetTreeStatusUrl(self, error_ok=False):
873 if not self.tree_status_url:
874 error_message = ('You must configure your tree status URL by running '
875 '"git cl config".')
[email protected]8b0553c2014-02-11 00:33:37876 self.tree_status_url = self._GetRietveldConfig(
877 'tree-status-url', error_ok=error_ok, error_message=error_message)
[email protected]cc51cd02010-12-23 00:48:39878 return self.tree_status_url
879
880 def GetViewVCUrl(self):
881 if not self.viewvc_url:
[email protected]8b0553c2014-02-11 00:33:37882 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
[email protected]cc51cd02010-12-23 00:48:39883 return self.viewvc_url
884
[email protected]90752582014-01-14 21:04:50885 def GetBugPrefix(self):
[email protected]8b0553c2014-02-11 00:33:37886 return self._GetRietveldConfig('bug-prefix', error_ok=True)
[email protected]90752582014-01-14 21:04:50887
[email protected]78948ed2015-07-08 23:09:57888 def GetIsSkipDependencyUpload(self, branch_name):
889 """Returns true if specified branch should skip dep uploads."""
890 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
891 error_ok=True)
892
[email protected]5626a922015-02-26 14:03:30893 def GetRunPostUploadHook(self):
894 run_post_upload_hook = self._GetRietveldConfig(
895 'run-post-upload-hook', error_ok=True)
896 return run_post_upload_hook == "True"
897
[email protected]ae6df352011-04-06 17:40:39898 def GetDefaultCCList(self):
[email protected]8b0553c2014-02-11 00:33:37899 return self._GetRietveldConfig('cc', error_ok=True)
[email protected]ae6df352011-04-06 17:40:39900
[email protected]c1737d02013-05-29 14:17:28901 def GetDefaultPrivateFlag(self):
[email protected]8b0553c2014-02-11 00:33:37902 return self._GetRietveldConfig('private', error_ok=True)
[email protected]c1737d02013-05-29 14:17:28903
[email protected]e8077812012-02-03 03:41:46904 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 20:39:42905 """Return true if this repo is associated with gerrit code review system."""
[email protected]e8077812012-02-03 03:41:46906 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 17:53:51907 self.is_gerrit = (
908 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
[email protected]e8077812012-02-03 03:41:46909 return self.is_gerrit
910
[email protected]54b400c2016-01-14 10:08:25911 def GetSquashGerritUploads(self):
912 """Return true if uploads to Gerrit should be squashed by default."""
913 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 09:01:53914 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
915 if self.squash_gerrit_uploads is None:
916 # Default is squash now (http://crbug.com/611892#c23).
917 self.squash_gerrit_uploads = not (
918 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
919 error_ok=True).strip() == 'false')
[email protected]54b400c2016-01-14 10:08:25920 return self.squash_gerrit_uploads
921
tandriia60502f2016-06-20 09:01:53922 def GetSquashGerritUploadsOverride(self):
923 """Return True or False if codereview.settings should be overridden.
924
925 Returns None if no override has been defined.
926 """
927 # See also http://crbug.com/611892#c23
928 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
929 error_ok=True).strip()
930 if result == 'true':
931 return True
932 if result == 'false':
933 return False
934 return None
935
[email protected]28253532016-04-14 13:46:56936 def GetGerritSkipEnsureAuthenticated(self):
937 """Return True if EnsureAuthenticated should not be done for Gerrit
938 uploads."""
939 if self.gerrit_skip_ensure_authenticated is None:
940 self.gerrit_skip_ensure_authenticated = (
[email protected]00dbccd2016-04-15 07:24:43941 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
[email protected]28253532016-04-14 13:46:56942 error_ok=True).strip() == 'true')
943 return self.gerrit_skip_ensure_authenticated
944
[email protected]615a2622013-05-03 13:20:14945 def GetGitEditor(self):
946 """Return the editor specified in the git config, or None if none is."""
947 if self.git_editor is None:
948 self.git_editor = self._GetConfig('core.editor', error_ok=True)
949 return self.git_editor or None
950
[email protected]44202a22014-03-11 19:22:18951 def GetLintRegex(self):
952 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
953 DEFAULT_LINT_REGEX)
954
955 def GetLintIgnoreRegex(self):
956 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
957 DEFAULT_LINT_IGNORE_REGEX)
958
[email protected]152cf832014-06-11 21:37:49959 def GetProject(self):
960 if not self.project:
961 self.project = self._GetRietveldConfig('project', error_ok=True)
962 return self.project
963
[email protected]8b0553c2014-02-11 00:33:37964 def _GetRietveldConfig(self, param, **kwargs):
965 return self._GetConfig('rietveld.' + param, **kwargs)
966
[email protected]78948ed2015-07-08 23:09:57967 def _GetBranchConfig(self, branch_name, param, **kwargs):
968 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
969
[email protected]cc51cd02010-12-23 00:48:39970 def _GetConfig(self, param, **kwargs):
971 self.LazyUpdateIfNeeded()
972 return RunGit(['config', param], **kwargs).strip()
973
974
Andrii Shyshkalovf3a20ae2017-01-24 20:23:57975@contextlib.contextmanager
976def _get_gerrit_project_config_file(remote_url):
977 """Context manager to fetch and store Gerrit's project.config from
978 refs/meta/config branch and store it in temp file.
979
980 Provides a temporary filename or None if there was error.
981 """
982 error, _ = RunGitWithCode([
983 'fetch', remote_url,
984 '+refs/meta/config:refs/git_cl/meta/config'])
985 if error:
986 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 20:39:42987 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 20:23:57988 (remote_url, error))
989 yield None
990 return
991
992 error, project_config_data = RunGitWithCode(
993 ['show', 'refs/git_cl/meta/config:project.config'])
994 if error:
995 print('WARNING: project.config file not found')
996 yield None
997 return
998
999 with gclient_utils.temporary_directory() as tempdir:
1000 project_config_file = os.path.join(tempdir, 'project.config')
1001 gclient_utils.FileWrite(project_config_file, project_config_data)
1002 yield project_config_file
1003
1004
[email protected]cc51cd02010-12-23 00:48:391005def ShortBranchName(branch):
1006 """Convert a name like 'refs/heads/foo' to just 'foo'."""
[email protected]aa5ced12016-03-29 09:41:141007 return branch.replace('refs/heads/', '', 1)
1008
1009
1010def GetCurrentBranchRef():
1011 """Returns branch ref (e.g., refs/heads/master) or None."""
1012 return RunGit(['symbolic-ref', 'HEAD'],
1013 stderr=subprocess2.VOID, error_ok=True).strip() or None
1014
1015
1016def GetCurrentBranch():
1017 """Returns current branch or None.
1018
1019 For refs/heads/* branches, returns just last part. For others, full ref.
1020 """
1021 branchref = GetCurrentBranchRef()
1022 if branchref:
1023 return ShortBranchName(branchref)
1024 return None
[email protected]cc51cd02010-12-23 00:48:391025
1026
[email protected]fa330e82016-04-13 17:09:521027class _CQState(object):
1028 """Enum for states of CL with respect to Commit Queue."""
1029 NONE = 'none'
1030 DRY_RUN = 'dry_run'
1031 COMMIT = 'commit'
1032
1033 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1034
1035
[email protected]f86c7d32016-04-01 19:27:301036class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 14:10:211037 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
[email protected]f86c7d32016-04-01 19:27:301038 self.issue = issue
1039 self.patchset = patchset
1040 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:231041 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 14:10:211042 self.codereview = codereview
[email protected]f86c7d32016-04-01 19:27:301043
1044 @property
1045 def valid(self):
1046 return self.issue is not None
1047
1048
Andrii Shyshkalovc9712392017-04-11 11:35:211049def ParseIssueNumberArgument(arg, codereview=None):
[email protected]f86c7d32016-04-01 19:27:301050 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1051 fail_result = _ParsedIssueNumberArgument()
1052
1053 if arg.isdigit():
Aaron Gableaee6c852017-06-26 19:49:011054 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
[email protected]f86c7d32016-04-01 19:27:301055 if not arg.startswith('http'):
1056 return fail_result
Aaron Gableaee6c852017-06-26 19:49:011057
[email protected]f86c7d32016-04-01 19:27:301058 url = gclient_utils.UpgradeToHttps(arg)
1059 try:
1060 parsed_url = urlparse.urlparse(url)
1061 except ValueError:
1062 return fail_result
Andrii Shyshkalov28d840e2017-04-10 13:45:091063
Andrii Shyshkalovc9712392017-04-11 11:35:211064 if codereview is not None:
1065 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1066 return parsed or fail_result
1067
Andrii Shyshkalov28d840e2017-04-10 13:45:091068 results = {}
1069 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1070 parsed = cls.ParseIssueURL(parsed_url)
1071 if parsed is not None:
1072 results[name] = parsed
1073
1074 if not results:
1075 return fail_result
1076 if len(results) == 1:
1077 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 11:35:211078
Andrii Shyshkalovf5569d22018-10-15 03:35:231079 return results['gerrit']
[email protected]f86c7d32016-04-01 19:27:301080
1081
Andrii Shyshkalovb07575f2018-10-16 06:16:211082def _create_description_from_log(args):
1083 """Pulls out the commit log to use as a base for the CL description."""
1084 log_args = []
1085 if len(args) == 1 and not args[0].endswith('.'):
1086 log_args = [args[0] + '..']
1087 elif len(args) == 1 and args[0].endswith('...'):
1088 log_args = [args[0][:-1]]
1089 elif len(args) == 2:
1090 log_args = [args[0] + '..' + args[1]]
1091 else:
1092 log_args = args[:] # Hope for the best!
1093 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1094
1095
Aaron Gablea45ee112016-11-22 23:14:381096class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 15:13:151097 def __init__(self, issue, url):
1098 self.issue = issue
1099 self.url = url
Aaron Gablea45ee112016-11-22 23:14:381100 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 15:13:151101
1102 def __str__(self):
Aaron Gablea45ee112016-11-22 23:14:381103 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 15:13:151104 self.issue, self.url)
1105
1106
Andrii Shyshkalovd8aa49f2017-03-17 15:05:491107_CommentSummary = collections.namedtuple(
1108 '_CommentSummary', ['date', 'message', 'sender',
1109 # TODO(tandrii): these two aren't known in Gerrit.
1110 'approval', 'disapproval'])
1111
1112
[email protected]cc51cd02010-12-23 00:48:391113class Changelist(object):
[email protected]aa5ced12016-03-29 09:41:141114 """Changelist works with one changelist in local branch.
1115
1116 Supports two codereview backends: Rietveld or Gerrit, selected at object
1117 creation.
1118
[email protected]8930b3d2016-04-13 14:47:021119 Notes:
1120 * Not safe for concurrent multi-{thread,process} use.
1121 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 23:19:371122 with great care.
[email protected]aa5ced12016-03-29 09:41:141123 """
1124
1125 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1126 """Create a new ChangeList instance.
1127
1128 If issue is given, the codereview must be given too.
1129
1130 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1131 Otherwise, it's decided based on current configuration of the local branch,
1132 with default being 'rietveld' for backwards compatibility.
1133 See _load_codereview_impl for more details.
1134
1135 **kwargs will be passed directly to codereview implementation.
1136 """
[email protected]cc51cd02010-12-23 00:48:391137 # Poke settings so we get the "configure your server" message if necessary.
[email protected]379d07a2011-11-30 14:58:101138 global settings
1139 if not settings:
1140 # Happens when git_cl.py is used as a utility library.
1141 settings = Settings()
[email protected]aa5ced12016-03-29 09:41:141142
1143 if issue:
1144 assert codereview, 'codereview must be known, if issue is known'
1145
[email protected]cc51cd02010-12-23 00:48:391146 self.branchref = branchref
1147 if self.branchref:
[email protected]cbd7dc32016-05-31 10:33:501148 assert branchref.startswith('refs/heads/')
[email protected]cc51cd02010-12-23 00:48:391149 self.branch = ShortBranchName(self.branchref)
1150 else:
1151 self.branch = None
[email protected]cc51cd02010-12-23 00:48:391152 self.upstream_branch = None
[email protected]1033efd2013-07-23 23:25:091153 self.lookedup_issue = False
1154 self.issue = issue or None
[email protected]cc51cd02010-12-23 00:48:391155 self.has_description = False
1156 self.description = None
[email protected]1033efd2013-07-23 23:25:091157 self.lookedup_patchset = False
[email protected]cc51cd02010-12-23 00:48:391158 self.patchset = None
[email protected]ae6df352011-04-06 17:40:391159 self.cc = None
Daniel Cheng7227d212017-11-17 16:12:371160 self.more_cc = []
[email protected]cf6a5d22015-04-09 22:02:001161 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:411162 self._cached_remote_url = (False, None) # (is_cached, value)
[email protected]cf6a5d22015-04-09 22:02:001163
[email protected]aa5ced12016-03-29 09:41:141164 self._codereview_impl = None
[email protected]d68b62b2016-03-31 16:09:291165 self._codereview = None
[email protected]aa5ced12016-03-29 09:41:141166 self._load_codereview_impl(codereview, **kwargs)
[email protected]d68b62b2016-03-31 16:09:291167 assert self._codereview_impl
1168 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
[email protected]aa5ced12016-03-29 09:41:141169
1170 def _load_codereview_impl(self, codereview=None, **kwargs):
1171 if codereview:
[email protected]d68b62b2016-03-31 16:09:291172 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1173 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1174 self._codereview = codereview
1175 self._codereview_impl = cls(self, **kwargs)
[email protected]aa5ced12016-03-29 09:41:141176 return
1177
1178 # Automatic selection based on issue number set for a current branch.
1179 # Rietveld takes precedence over Gerrit.
1180 assert not self.issue
1181 # Whether we find issue or not, we are doing the lookup.
1182 self.lookedup_issue = True
tandrii5d48c322016-08-18 23:19:371183 if self.GetBranch():
1184 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1185 issue = _git_get_branch_config_value(
1186 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1187 if issue:
1188 self._codereview = codereview
1189 self._codereview_impl = cls(self, **kwargs)
1190 self.issue = int(issue)
1191 return
[email protected]aa5ced12016-03-29 09:41:141192
1193 # No issue is set for this branch, so decide based on repo-wide settings.
1194 return self._load_codereview_impl(
1195 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1196 **kwargs)
1197
[email protected]d68b62b2016-03-31 16:09:291198 def IsGerrit(self):
1199 return self._codereview == 'gerrit'
[email protected]ae6df352011-04-06 17:40:391200
1201 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 20:39:421202 """Returns the users cc'd on this CL.
[email protected]ae6df352011-04-06 17:40:391203
Quinten Yearsley0c62da92017-05-31 20:39:421204 The return value is a string suitable for passing to git cl with the --cc
1205 flag.
[email protected]ae6df352011-04-06 17:40:391206 """
1207 if self.cc is None:
[email protected]99918ab2013-09-30 06:17:281208 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 16:12:371209 more_cc = ','.join(self.more_cc)
[email protected]ae6df352011-04-06 17:40:391210 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1211 return self.cc
1212
[email protected]99918ab2013-09-30 06:17:281213 def GetCCListWithoutDefault(self):
1214 """Return the users cc'd on this CL excluding default ones."""
1215 if self.cc is None:
Daniel Cheng7227d212017-11-17 16:12:371216 self.cc = ','.join(self.more_cc)
[email protected]99918ab2013-09-30 06:17:281217 return self.cc
1218
Daniel Cheng7227d212017-11-17 16:12:371219 def ExtendCC(self, more_cc):
1220 """Extends the list of users to cc on this CL based on the changed files."""
1221 self.more_cc.extend(more_cc)
[email protected]cc51cd02010-12-23 00:48:391222
1223 def GetBranch(self):
1224 """Returns the short branch name, e.g. 'master'."""
1225 if not self.branch:
[email protected]aa5ced12016-03-29 09:41:141226 branchref = GetCurrentBranchRef()
[email protected]d62c61f2014-10-20 22:33:211227 if not branchref:
1228 return None
1229 self.branchref = branchref
[email protected]cc51cd02010-12-23 00:48:391230 self.branch = ShortBranchName(self.branchref)
1231 return self.branch
1232
1233 def GetBranchRef(self):
1234 """Returns the full branch name, e.g. 'refs/heads/master'."""
1235 self.GetBranch() # Poke the lazy loader.
1236 return self.branchref
1237
[email protected]534f67a2016-04-07 18:47:051238 def ClearBranch(self):
1239 """Clears cached branch data of this object."""
1240 self.branch = self.branchref = None
1241
tandrii5d48c322016-08-18 23:19:371242 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1243 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1244 kwargs['branch'] = self.GetBranch()
1245 return _git_get_branch_config_value(key, default, **kwargs)
1246
1247 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1248 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1249 assert self.GetBranch(), (
1250 'this CL must have an associated branch to %sset %s%s' %
1251 ('un' if value is None else '',
1252 key,
1253 '' if value is None else ' to %r' % value))
1254 kwargs['branch'] = self.GetBranch()
1255 return _git_set_branch_config_value(key, value, **kwargs)
1256
[email protected]0f58fa82012-11-05 01:45:201257 @staticmethod
1258 def FetchUpstreamTuple(branch):
[email protected]d6617f32013-11-19 00:34:541259 """Returns a tuple containing remote and remote ref,
[email protected]cc51cd02010-12-23 00:48:391260 e.g. 'origin', 'refs/heads/master'
1261 """
1262 remote = '.'
tandrii5d48c322016-08-18 23:19:371263 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1264
[email protected]cc51cd02010-12-23 00:48:391265 if upstream_branch:
tandrii5d48c322016-08-18 23:19:371266 remote = _git_get_branch_config_value('remote', branch=branch)
[email protected]cc51cd02010-12-23 00:48:391267 else:
[email protected]ade368c2011-03-01 08:57:501268 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1269 error_ok=True).strip()
1270 if upstream_branch:
1271 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
[email protected]cc51cd02010-12-23 00:48:391272 else:
Aaron Gable1bc7bfe2016-12-19 18:08:141273 # Else, try to guess the origin remote.
1274 remote_branches = RunGit(['branch', '-r']).split()
1275 if 'origin/master' in remote_branches:
1276 # Fall back on origin/master if it exits.
1277 remote = 'origin'
1278 upstream_branch = 'refs/heads/master'
[email protected]cc51cd02010-12-23 00:48:391279 else:
Aaron Gable1bc7bfe2016-12-19 18:08:141280 DieWithError(
1281 'Unable to determine default branch to diff against.\n'
1282 'Either pass complete "git diff"-style arguments, like\n'
1283 ' git cl upload origin/master\n'
1284 'or verify this branch is set up to track another \n'
1285 '(via the --track argument to "git checkout -b ...").')
[email protected]cc51cd02010-12-23 00:48:391286
1287 return remote, upstream_branch
1288
[email protected]8b0553c2014-02-11 00:33:371289 def GetCommonAncestorWithUpstream(self):
[email protected]8ba38ff2015-06-11 21:41:251290 upstream_branch = self.GetUpstreamBranch()
1291 if not BranchExists(upstream_branch):
1292 DieWithError('The upstream for the current branch (%s) does not exist '
1293 'anymore.\nPlease fix it and try again.' % self.GetBranch())
[email protected]9e849272014-04-04 00:31:551294 return git_common.get_or_create_merge_base(self.GetBranch(),
[email protected]8ba38ff2015-06-11 21:41:251295 upstream_branch)
[email protected]8b0553c2014-02-11 00:33:371296
[email protected]cc51cd02010-12-23 00:48:391297 def GetUpstreamBranch(self):
1298 if self.upstream_branch is None:
[email protected]0f58fa82012-11-05 01:45:201299 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
[email protected]cc51cd02010-12-23 00:48:391300 if remote is not '.':
[email protected]e7585452014-08-24 01:41:111301 upstream_branch = upstream_branch.replace('refs/heads/',
1302 'refs/remotes/%s/' % remote)
1303 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1304 'refs/remotes/branch-heads/')
[email protected]cc51cd02010-12-23 00:48:391305 self.upstream_branch = upstream_branch
1306 return self.upstream_branch
1307
[email protected]0f58fa82012-11-05 01:45:201308 def GetRemoteBranch(self):
[email protected]a2cbbbb2012-03-22 20:40:401309 if not self._remote:
[email protected]0f58fa82012-11-05 01:45:201310 remote, branch = None, self.GetBranch()
1311 seen_branches = set()
1312 while branch not in seen_branches:
1313 seen_branches.add(branch)
1314 remote, branch = self.FetchUpstreamTuple(branch)
1315 branch = ShortBranchName(branch)
1316 if remote != '.' or branch.startswith('refs/remotes'):
1317 break
1318 else:
[email protected]a2cbbbb2012-03-22 20:40:401319 remotes = RunGit(['remote'], error_ok=True).split()
1320 if len(remotes) == 1:
[email protected]0f58fa82012-11-05 01:45:201321 remote, = remotes
[email protected]a2cbbbb2012-03-22 20:40:401322 elif 'origin' in remotes:
[email protected]0f58fa82012-11-05 01:45:201323 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 18:08:141324 logging.warn('Could not determine which remote this change is '
1325 'associated with, so defaulting to "%s".' % self._remote)
[email protected]a2cbbbb2012-03-22 20:40:401326 else:
1327 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 18:08:141328 'associated with.')
[email protected]0f58fa82012-11-05 01:45:201329 branch = 'HEAD'
1330 if branch.startswith('refs/remotes'):
1331 self._remote = (remote, branch)
[email protected]e7585452014-08-24 01:41:111332 elif branch.startswith('refs/branch-heads/'):
1333 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
[email protected]0f58fa82012-11-05 01:45:201334 else:
1335 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
[email protected]a2cbbbb2012-03-22 20:40:401336 return self._remote
1337
[email protected]0f58fa82012-11-05 01:45:201338 def GitSanityChecks(self, upstream_git_obj):
1339 """Checks git repo status and ensures diff is from local commits."""
1340
[email protected]79706062015-01-14 21:18:121341 if upstream_git_obj is None:
1342 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 20:39:421343 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 16:17:491344 file=sys.stderr)
[email protected]79706062015-01-14 21:18:121345 else:
Quinten Yearsley0c62da92017-05-31 20:39:421346 print('ERROR: No upstream branch.', file=sys.stderr)
[email protected]79706062015-01-14 21:18:121347 return False
1348
[email protected]0f58fa82012-11-05 01:45:201349 # Verify the commit we're diffing against is in our current branch.
1350 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1351 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1352 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 16:17:491353 print('ERROR: %s is not in the current branch. You may need to rebase '
1354 'your tracking branch' % upstream_sha, file=sys.stderr)
[email protected]0f58fa82012-11-05 01:45:201355 return False
1356
1357 # List the commits inside the diff, and verify they are all local.
1358 commits_in_diff = RunGit(
1359 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1360 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1361 remote_branch = remote_branch.strip()
1362 if code != 0:
1363 _, remote_branch = self.GetRemoteBranch()
1364
1365 commits_in_remote = RunGit(
1366 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1367
1368 common_commits = set(commits_in_diff) & set(commits_in_remote)
1369 if common_commits:
vapiera7fbd5a2016-06-16 16:17:491370 print('ERROR: Your diff contains %d commits already in %s.\n'
1371 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1372 'the diff. If you are using a custom git flow, you can override'
1373 ' the reference used for this check with "git config '
1374 'gitcl.remotebranch <git-ref>".' % (
1375 len(common_commits), remote_branch, upstream_git_obj),
1376 file=sys.stderr)
[email protected]0f58fa82012-11-05 01:45:201377 return False
1378 return True
1379
[email protected]6b0051e2012-04-03 15:45:081380 def GetGitBaseUrlFromConfig(self):
[email protected]a656e702014-05-15 20:43:051381 """Return the configured base URL from branch.<branchname>.baseurl.
[email protected]6b0051e2012-04-03 15:45:081382
1383 Returns None if it is not set.
1384 """
tandrii5d48c322016-08-18 23:19:371385 return self._GitGetBranchConfigValue('base-url')
[email protected]a2cbbbb2012-03-22 20:40:401386
[email protected]cc51cd02010-12-23 00:48:391387 def GetRemoteUrl(self):
1388 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1389
1390 Returns None if there is no remote.
1391 """
Andrii Shyshkalov81db1d52018-08-23 02:17:411392 is_cached, value = self._cached_remote_url
1393 if is_cached:
1394 return value
1395
[email protected]0f58fa82012-11-05 01:45:201396 remote, _ = self.GetRemoteBranch()
[email protected]2a13d4f2014-06-13 00:06:371397 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1398
1399 # If URL is pointing to a local directory, it is probably a git cache.
1400 if os.path.isdir(url):
1401 url = RunGit(['config', 'remote.%s.url' % remote],
1402 error_ok=True,
1403 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:411404 self._cached_remote_url = (True, url)
[email protected]2a13d4f2014-06-13 00:06:371405 return url
[email protected]cc51cd02010-12-23 00:48:391406
[email protected]87985d22016-03-24 17:33:331407 def GetIssue(self):
[email protected]52424302012-08-29 15:14:301408 """Returns the issue number as a int or None if not set."""
[email protected]87985d22016-03-24 17:33:331409 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 23:19:371410 self.issue = self._GitGetBranchConfigValue(
1411 self._codereview_impl.IssueConfigKey(), value_type=int)
[email protected]1033efd2013-07-23 23:25:091412 self.lookedup_issue = True
[email protected]cc51cd02010-12-23 00:48:391413 return self.issue
1414
[email protected]cc51cd02010-12-23 00:48:391415 def GetIssueURL(self):
1416 """Get the URL for a particular issue."""
[email protected]aa5ced12016-03-29 09:41:141417 issue = self.GetIssue()
1418 if not issue:
[email protected]015fd3d2013-06-18 19:02:501419 return None
[email protected]aa5ced12016-03-29 09:41:141420 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
[email protected]cc51cd02010-12-23 00:48:391421
Andrii Shyshkalov31863012017-02-08 10:35:121422 def GetDescription(self, pretty=False, force=False):
1423 if not self.has_description or force:
[email protected]cc51cd02010-12-23 00:48:391424 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 19:47:131425 self.description = self._codereview_impl.FetchDescription(force=force)
[email protected]cc51cd02010-12-23 00:48:391426 self.has_description = True
1427 if pretty:
Asanka Herath7c61dcb2016-12-14 18:01:581428 # Set width to 72 columns + 2 space indent.
1429 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
[email protected]cc51cd02010-12-23 00:48:391430 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 18:01:581431 lines = self.description.splitlines()
1432 return '\n'.join([wrapper.fill(line) for line in lines])
[email protected]cc51cd02010-12-23 00:48:391433 return self.description
1434
Robert Iannucci09f1f3d2017-03-28 23:54:321435 def GetDescriptionFooters(self):
1436 """Returns (non_footer_lines, footers) for the commit message.
1437
1438 Returns:
1439 non_footer_lines (list(str)) - Simple list of description lines without
1440 any footer. The lines do not contain newlines, nor does the list contain
1441 the empty line between the message and the footers.
1442 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1443 [("Change-Id", "Ideadbeef...."), ...]
1444 """
1445 raw_description = self.GetDescription()
1446 msg_lines, _, footers = git_footers.split_footers(raw_description)
1447 if footers:
1448 msg_lines = msg_lines[:len(msg_lines)-1]
1449 return msg_lines, footers
1450
[email protected]cc51cd02010-12-23 00:48:391451 def GetPatchset(self):
[email protected]52424302012-08-29 15:14:301452 """Returns the patchset number as a int or None if not set."""
[email protected]1033efd2013-07-23 23:25:091453 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 23:19:371454 self.patchset = self._GitGetBranchConfigValue(
1455 self._codereview_impl.PatchsetConfigKey(), value_type=int)
[email protected]1033efd2013-07-23 23:25:091456 self.lookedup_patchset = True
[email protected]cc51cd02010-12-23 00:48:391457 return self.patchset
1458
1459 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 23:19:371460 """Set this branch's patchset. If patchset=0, clears the patchset."""
1461 assert self.GetBranch()
1462 if not patchset:
[email protected]1033efd2013-07-23 23:25:091463 self.patchset = None
tandrii5d48c322016-08-18 23:19:371464 else:
1465 self.patchset = int(patchset)
1466 self._GitSetBranchConfigValue(
1467 self._codereview_impl.PatchsetConfigKey(), self.patchset)
[email protected]cc51cd02010-12-23 00:48:391468
[email protected]a342c922016-03-16 07:08:251469 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 23:19:371470 """Set this branch's issue. If issue isn't given, clears the issue."""
1471 assert self.GetBranch()
[email protected]cc51cd02010-12-23 00:48:391472 if issue:
tandrii5d48c322016-08-18 23:19:371473 issue = int(issue)
1474 self._GitSetBranchConfigValue(
1475 self._codereview_impl.IssueConfigKey(), issue)
[email protected]1033efd2013-07-23 23:25:091476 self.issue = issue
[email protected]aa5ced12016-03-29 09:41:141477 codereview_server = self._codereview_impl.GetCodereviewServer()
1478 if codereview_server:
tandrii5d48c322016-08-18 23:19:371479 self._GitSetBranchConfigValue(
1480 self._codereview_impl.CodereviewServerConfigKey(),
1481 codereview_server)
[email protected]cc51cd02010-12-23 00:48:391482 else:
tandrii5d48c322016-08-18 23:19:371483 # Reset all of these just to be clean.
1484 reset_suffixes = [
1485 'last-upload-hash',
1486 self._codereview_impl.IssueConfigKey(),
1487 self._codereview_impl.PatchsetConfigKey(),
1488 self._codereview_impl.CodereviewServerConfigKey(),
1489 ] + self._PostUnsetIssueProperties()
1490 for prop in reset_suffixes:
1491 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 18:16:021492 msg = RunGit(['log', '-1', '--format=%B']).strip()
1493 if msg and git_footers.get_footer_change_id(msg):
1494 print('WARNING: The change patched into this branch has a Change-Id. '
1495 'Removing it.')
1496 RunGit(['commit', '--amend', '-m',
1497 git_footers.remove_footer(msg, 'Change-Id')])
[email protected]1033efd2013-07-23 23:25:091498 self.issue = None
[email protected]9b7fd712016-06-01 13:45:201499 self.patchset = None
[email protected]cc51cd02010-12-23 00:48:391500
dnjba1b0f32016-09-02 19:37:421501 def GetChange(self, upstream_branch, author, local_description=False):
[email protected]0f58fa82012-11-05 01:45:201502 if not self.GitSanityChecks(upstream_branch):
1503 DieWithError('\nGit sanity check failure')
1504
[email protected]8b0553c2014-02-11 00:33:371505 root = settings.GetRelativeRoot()
[email protected]f267b0e2013-05-02 09:11:431506 if not root:
1507 root = '.'
[email protected]512f1ef2011-04-20 15:17:571508 absroot = os.path.abspath(root)
[email protected]6fb99c62011-04-18 15:57:281509
1510 # We use the sha1 of HEAD as a name of this change.
[email protected]8b0553c2014-02-11 00:33:371511 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
[email protected]512f1ef2011-04-20 15:17:571512 # Need to pass a relative path for msysgit.
[email protected]2b38e9c2011-10-19 00:04:351513 try:
[email protected]80a9ef12011-12-13 20:44:101514 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
[email protected]2b38e9c2011-10-19 00:04:351515 except subprocess2.CalledProcessError:
1516 DieWithError(
[email protected]d6617f32013-11-19 00:34:541517 ('\nFailed to diff against upstream branch %s\n\n'
[email protected]2b38e9c2011-10-19 00:04:351518 'This branch probably doesn\'t exist anymore. To reset the\n'
1519 'tracking branch, please run\n'
stip7a3dd352016-09-23 00:32:281520 ' git branch --set-upstream-to origin/master %s\n'
1521 'or replace origin/master with the relevant branch') %
[email protected]2b38e9c2011-10-19 00:04:351522 (upstream_branch, self.GetBranch()))
[email protected]6fb99c62011-04-18 15:57:281523
[email protected]52424302012-08-29 15:14:301524 issue = self.GetIssue()
1525 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 19:37:421526 if issue and not local_description:
[email protected]6fb99c62011-04-18 15:57:281527 description = self.GetDescription()
1528 else:
1529 # If the change was never uploaded, use the log messages of all commits
1530 # up to the branch point, as git cl upload will prefill the description
1531 # with these log messages.
[email protected]8b0553c2014-02-11 00:33:371532 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1533 description = RunGitWithCode(args)[1].strip()
[email protected]03b3bdc2011-06-14 13:04:121534
1535 if not author:
[email protected]13f623c2011-07-22 16:02:231536 author = RunGit(['config', 'user.email']).strip() or None
[email protected]15169952011-09-27 14:30:531537 return presubmit_support.GitChange(
[email protected]6fb99c62011-04-18 15:57:281538 name,
1539 description,
1540 absroot,
1541 files,
1542 issue,
1543 patchset,
[email protected]ea84ef12014-04-30 19:55:121544 author,
1545 upstream=upstream_branch)
[email protected]6fb99c62011-04-18 15:57:281546
dsansomee2d6fd92016-09-08 07:10:471547 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 22:47:291548 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
[email protected]aa5ced12016-03-29 09:41:141549 self.description = description
Andrii Shyshkalov83051152017-02-07 22:47:291550 self.has_description = True
[email protected]aa5ced12016-03-29 09:41:141551
Robert Iannucci09f1f3d2017-03-28 23:54:321552 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1553 """Sets the description for this CL remotely.
1554
1555 You can get description_lines and footers with GetDescriptionFooters.
1556
1557 Args:
1558 description_lines (list(str)) - List of CL description lines without
1559 newline characters.
1560 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1561 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1562 `List-Of-Tokens`). It will be case-normalized so that each token is
1563 title-cased.
1564 """
1565 new_description = '\n'.join(description_lines)
1566 if footers:
1567 new_description += '\n'
1568 for k, v in footers:
1569 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1570 if not git_footers.FOOTER_PATTERN.match(foot):
1571 raise ValueError('Invalid footer %r' % foot)
1572 new_description += foot + '\n'
1573 self.UpdateDescription(new_description, force)
1574
Edward Lesmes8e282792018-04-03 22:50:291575 def RunHook(self, committing, may_prompt, verbose, change, parallel):
[email protected]aa5ced12016-03-29 09:41:141576 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1577 try:
1578 return presubmit_support.DoPresubmitChecks(change, committing,
1579 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1580 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 22:50:291581 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1582 parallel=parallel)
vapierfd77ac72016-06-16 15:33:571583 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 17:12:411584 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
[email protected]aa5ced12016-03-29 09:41:141585
[email protected]f86c7d32016-04-01 19:27:301586 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1587 """Fetches and applies the issue patch from codereview to local branch."""
[email protected]ef7c68c2016-04-07 09:39:391588 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1589 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
[email protected]f86c7d32016-04-01 19:27:301590 else:
1591 # Assume url.
1592 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1593 urlparse.urlparse(issue_arg))
1594 if not parsed_issue_arg or not parsed_issue_arg.valid:
1595 DieWithError('Failed to parse issue argument "%s". '
1596 'Must be an issue number or a valid URL.' % issue_arg)
1597 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 15:22:091598 parsed_issue_arg, reject, nocommit, directory, False)
[email protected]f86c7d32016-04-01 19:27:301599
[email protected]9e6c3a52016-04-12 14:13:081600 def CMDUpload(self, options, git_diff_args, orig_args):
1601 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:231602 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 15:14:491603 custom_cl_base = None
[email protected]9e6c3a52016-04-12 14:13:081604 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 15:14:491605 custom_cl_base = base_branch = git_diff_args[0]
[email protected]9e6c3a52016-04-12 14:13:081606 else:
1607 if self.GetBranch() is None:
1608 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1609
1610 # Default to diffing against common ancestor of upstream branch
1611 base_branch = self.GetCommonAncestorWithUpstream()
1612 git_diff_args = [base_branch, 'HEAD']
1613
Aaron Gablec4c40d12017-05-22 18:49:531614
Andrii Shyshkalov3e631422017-02-16 16:46:441615 # 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.
[email protected]fe30f182016-04-13 12:15:041618 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 13:59:281619 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
[email protected]9e6c3a52016-04-12 14:13:081620
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 16:12:371626 self.ExtendCC(watchlist.GetWatchersForPaths(files))
[email protected]9e6c3a52016-04-12 14:13:081627
1628 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 18:38:001629 if options.reviewers or options.tbrs or options.add_owners_to:
[email protected]9e6c3a52016-04-12 14:13:081630 # 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 18:38:001633 options.tbrs,
Robert Iannuccif2708bd2017-04-17 22:49:021634 options.add_owners_to,
[email protected]9e6c3a52016-04-12 14:13:081635 change)
1636 change.SetDescriptionText(change_description.description)
1637 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 22:50:291638 may_prompt=not options.force,
1639 verbose=options.verbose,
1640 change=change, parallel=options.parallel)
[email protected]9e6c3a52016-04-12 14:13:081641 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 16:12:371645 self.ExtendCC(hook_results.more_cc)
[email protected]9e6c3a52016-04-12 14:13:081646
Aaron Gable13101a62018-02-09 21:20:411647 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 15:14:491648 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
[email protected]9e6c3a52016-04-12 14:13:081649 if not ret:
tandrii5d48c322016-08-18 23:19:371650 _git_set_branch_config_value('last-upload-hash',
1651 RunGit(['rev-parse', 'HEAD']).strip())
[email protected]9e6c3a52016-04-12 14:13:081652 # Run post upload hooks, if specified.
1653 if settings.GetRunPostUploadHook():
1654 presubmit_support.DoPostUploadExecuter(
1655 change,
1656 self,
1657 settings.GetRoot(),
1658 options.verbose,
1659 sys.stdout)
1660
1661 # Upload all dependencies if specified.
1662 if options.dependencies:
vapiera7fbd5a2016-06-16 16:17:491663 print()
1664 print('--dependencies has been specified.')
1665 print('All dependent local branches will be re-uploaded.')
1666 print()
[email protected]9e6c3a52016-04-12 14:13:081667 # Remove the dependencies flag from args so that we do not end up in a
1668 # loop.
1669 orig_args.remove('--dependencies')
1670 ret = upload_branch_deps(self, orig_args)
1671 return ret
1672
Ravi Mistry31e7d562018-04-02 16:53:571673 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1674 """Sets labels on the change based on the provided flags.
1675
1676 Sets labels if issue is already uploaded and known, else returns without
1677 doing anything.
1678
1679 Args:
1680 enable_auto_submit: Sets Auto-Submit+1 on the change.
1681 use_commit_queue: Sets Commit-Queue+2 on the change.
1682 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1683 both use_commit_queue and cq_dry_run are true.
1684 """
1685 if not self.GetIssue():
1686 return
1687 try:
1688 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1689 cq_dry_run)
1690 return 0
1691 except KeyboardInterrupt:
1692 raise
1693 except:
1694 labels = []
1695 if enable_auto_submit:
1696 labels.append('Auto-Submit')
1697 if use_commit_queue or cq_dry_run:
1698 labels.append('Commit-Queue')
1699 print('WARNING: Failed to set label(s) on your change: %s\n'
1700 'Either:\n'
1701 ' * Your project does not have the above label(s),\n'
1702 ' * You don\'t have permission to set the above label(s),\n'
1703 ' * There\'s a bug in this code (see stack trace below).\n' %
1704 (', '.join(labels)))
1705 # Still raise exception so that stack trace is printed.
1706 raise
1707
[email protected]fa330e82016-04-13 17:09:521708 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 18:50:521709 """Updates the CQ state for the latest patchset.
[email protected]fa330e82016-04-13 17:09:521710
1711 Issue must have been already uploaded and known.
1712 """
1713 assert new_state in _CQState.ALL_STATES
1714 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 20:22:031715 try:
Quinten Yearsleyfc5fd922017-05-31 18:50:521716 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 20:22:031717 return 0
1718 except KeyboardInterrupt:
1719 raise
1720 except:
Quinten Yearsleyfc5fd922017-05-31 18:50:521721 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 20:22:031722 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 18:50:521723 ' * Your project has no CQ,\n'
1724 ' * You don\'t have permission to change the CQ state,\n'
1725 ' * There\'s a bug in this code (see stack trace below).\n'
1726 'Consider specifying which bots to trigger manually or asking your '
1727 'project owners for permissions or contacting Chrome Infra at:\n'
1728 'https://www.chromium.org/infra\n\n' %
1729 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 20:22:031730 # Still raise exception so that stack trace is printed.
1731 raise
1732
[email protected]aa5ced12016-03-29 09:41:141733 # Forward methods to codereview specific implementation.
1734
Aaron Gable636b13f2017-07-14 17:42:481735 def AddComment(self, message, publish=None):
1736 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-15 23:24:371737
Aaron Gable0ffdf2d2017-06-05 20:01:171738 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 15:05:491739 """Returns list of _CommentSummary for each comment.
1740
Aaron Gable0ffdf2d2017-06-05 20:01:171741 args:
1742 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 15:05:491743 """
Aaron Gable0ffdf2d2017-06-05 20:01:171744 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 15:05:491745
[email protected]aa5ced12016-03-29 09:41:141746 def CloseIssue(self):
1747 return self._codereview_impl.CloseIssue()
1748
1749 def GetStatus(self):
1750 return self._codereview_impl.GetStatus()
1751
1752 def GetCodereviewServer(self):
1753 return self._codereview_impl.GetCodereviewServer()
1754
tandriide281ae2016-10-12 13:02:301755 def GetIssueOwner(self):
1756 """Get owner from codereview, which may differ from this checkout."""
1757 return self._codereview_impl.GetIssueOwner()
1758
Edward Lemur707d70b2018-02-06 23:50:141759 def GetReviewers(self):
1760 return self._codereview_impl.GetReviewers()
1761
[email protected]aa5ced12016-03-29 09:41:141762 def GetMostRecentPatchset(self):
1763 return self._codereview_impl.GetMostRecentPatchset()
1764
tandriide281ae2016-10-12 13:02:301765 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 20:39:421766 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 13:02:301767 return self._codereview_impl.CannotTriggerTryJobReason()
1768
Quinten Yearsley0c62da92017-05-31 20:39:421769 def GetTryJobProperties(self, patchset=None):
1770 """Returns dictionary of properties to launch try job."""
1771 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 14:52:021772
[email protected]aa5ced12016-03-29 09:41:141773 def __getattr__(self, attr):
1774 # This is because lots of untested code accesses Rietveld-specific stuff
1775 # directly, and it's hard to fix for sure. So, just let it work, and fix
[email protected]cbd7dc32016-05-31 10:33:501776 # on a case by case basis.
tandrii4d895502016-08-18 15:26:191777 # Note that child method defines __getattr__ as well, and forwards it here,
1778 # because _RietveldChangelistImpl is not cleaned up yet, and given
1779 # deprecation of Rietveld, it should probably be just removed.
1780 # Until that time, avoid infinite recursion by bypassing __getattr__
1781 # of implementation class.
1782 return self._codereview_impl.__getattribute__(attr)
[email protected]aa5ced12016-03-29 09:41:141783
1784
1785class _ChangelistCodereviewBase(object):
1786 """Abstract base class encapsulating codereview specifics of a changelist."""
1787 def __init__(self, changelist):
1788 self._changelist = changelist # instance of Changelist
1789
1790 def __getattr__(self, attr):
1791 # Forward methods to changelist.
1792 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1793 # _RietveldChangelistImpl to avoid this hack?
1794 return getattr(self._changelist, attr)
1795
1796 def GetStatus(self):
1797 """Apply a rough heuristic to give a simple summary of an issue's review
1798 or CQ status, assuming adherence to a common workflow.
1799
1800 Returns None if no issue for this branch, or specific string keywords.
1801 """
1802 raise NotImplementedError()
1803
1804 def GetCodereviewServer(self):
1805 """Returns server URL without end slash, like "https://codereview.com"."""
1806 raise NotImplementedError()
1807
Kenneth Russell61e2ed42017-02-15 19:47:131808 def FetchDescription(self, force=False):
[email protected]aa5ced12016-03-29 09:41:141809 """Fetches and returns description from the codereview server."""
1810 raise NotImplementedError()
1811
tandrii5d48c322016-08-18 23:19:371812 @classmethod
1813 def IssueConfigKey(cls):
1814 """Returns branch setting storing issue number."""
[email protected]aa5ced12016-03-29 09:41:141815 raise NotImplementedError()
1816
[email protected]5df290f2016-04-11 16:12:291817 @classmethod
tandrii5d48c322016-08-18 23:19:371818 def PatchsetConfigKey(cls):
1819 """Returns branch setting storing patchset number."""
[email protected]aa5ced12016-03-29 09:41:141820 raise NotImplementedError()
1821
tandrii5d48c322016-08-18 23:19:371822 @classmethod
1823 def CodereviewServerConfigKey(cls):
1824 """Returns branch setting storing codereview server."""
[email protected]aa5ced12016-03-29 09:41:141825 raise NotImplementedError()
1826
[email protected]9b7fd712016-06-01 13:45:201827 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 20:39:421828 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 23:19:371829 return []
[email protected]9b7fd712016-06-01 13:45:201830
[email protected]37b07a72016-04-29 16:42:281831 def GetGerritObjForPresubmit(self):
1832 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1833 return None
1834
dsansomee2d6fd92016-09-08 07:10:471835 def UpdateDescriptionRemote(self, description, force=False):
[email protected]aa5ced12016-03-29 09:41:141836 """Update the description on codereview site."""
1837 raise NotImplementedError()
1838
Aaron Gable636b13f2017-07-14 17:42:481839 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-15 23:24:371840 """Posts a comment to the codereview site."""
1841 raise NotImplementedError()
1842
Aaron Gable0ffdf2d2017-06-05 20:01:171843 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 15:05:491844 raise NotImplementedError()
1845
[email protected]aa5ced12016-03-29 09:41:141846 def CloseIssue(self):
1847 """Closes the issue."""
1848 raise NotImplementedError()
1849
[email protected]aa5ced12016-03-29 09:41:141850 def GetMostRecentPatchset(self):
1851 """Returns the most recent patchset number from the codereview site."""
1852 raise NotImplementedError()
1853
[email protected]f86c7d32016-04-01 19:27:301854 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 15:22:091855 directory, force):
[email protected]f86c7d32016-04-01 19:27:301856 """Fetches and applies the issue.
1857
1858 Arguments:
1859 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1860 reject: if True, reject the failed patch instead of switching to 3-way
1861 merge. Rietveld only.
1862 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1863 only.
1864 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 15:22:091865 force: if true, overwrites existing local state.
[email protected]f86c7d32016-04-01 19:27:301866 """
1867 raise NotImplementedError()
1868
1869 @staticmethod
1870 def ParseIssueURL(parsed_url):
1871 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1872 failed."""
1873 raise NotImplementedError()
1874
Andrii Shyshkalov2f8e9242017-01-23 18:20:191875 def EnsureAuthenticated(self, force, refresh=False):
[email protected]fe30f182016-04-13 12:15:041876 """Best effort check that user is authenticated with codereview server.
1877
1878 Arguments:
1879 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 18:20:191880 refresh: whether to attempt to refresh credentials. Ignored if not
1881 applicable.
[email protected]fe30f182016-04-13 12:15:041882 """
[email protected]9e6c3a52016-04-12 14:13:081883 raise NotImplementedError()
1884
Andrii Shyshkalovbb86fbb2017-03-24 13:59:281885 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 16:46:441886 """Best effort check that uploading isn't supposed to fail for predictable
1887 reasons.
1888
1889 This method should raise informative exception if uploading shouldn't
1890 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 13:59:281891
1892 Arguments:
1893 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 16:46:441894 """
Andrii Shyshkalovbb86fbb2017-03-24 13:59:281895 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 16:46:441896
Andrii Shyshkalov550e9242017-04-12 15:14:491897 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
[email protected]aa6235b2016-04-11 21:35:291898 """Uploads a change to codereview."""
1899 raise NotImplementedError()
1900
Ravi Mistry31e7d562018-04-02 16:53:571901 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1902 """Sets labels on the change based on the provided flags.
1903
1904 Issue must have been already uploaded and known.
1905 """
1906 raise NotImplementedError()
1907
[email protected]fa330e82016-04-13 17:09:521908 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 20:39:421909 """Updates the CQ state for the latest patchset.
[email protected]fa330e82016-04-13 17:09:521910
1911 Issue must have been already uploaded and known.
1912 """
1913 raise NotImplementedError()
1914
tandriie113dfd2016-10-11 17:20:121915 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 20:39:421916 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 17:20:121917 raise NotImplementedError()
1918
tandriide281ae2016-10-12 13:02:301919 def GetIssueOwner(self):
1920 raise NotImplementedError()
1921
Edward Lemur707d70b2018-02-06 23:50:141922 def GetReviewers(self):
1923 raise NotImplementedError()
1924
Quinten Yearsley0c62da92017-05-31 20:39:421925 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 13:02:301926 raise NotImplementedError()
1927
[email protected]aa5ced12016-03-29 09:41:141928
1929class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 20:39:421930
Andrii Shyshkalov8039be72017-01-26 08:38:181931 def __init__(self, changelist, auth_config=None, codereview_host=None):
[email protected]aa5ced12016-03-29 09:41:141932 super(_RietveldChangelistImpl, self).__init__(changelist)
1933 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 08:38:181934 if not codereview_host:
martiniss6eda05f2016-06-30 17:18:351935 settings.GetDefaultServerUrl()
[email protected]aa5ced12016-03-29 09:41:141936
Andrii Shyshkalov8039be72017-01-26 08:38:181937 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 12:57:521938 self._auth_config = auth_config or auth.make_auth_config()
[email protected]aa5ced12016-03-29 09:41:141939 self._props = None
1940 self._rpc_server = None
1941
[email protected]aa5ced12016-03-29 09:41:141942 def GetCodereviewServer(self):
1943 if not self._rietveld_server:
1944 # If we're on a branch then get the server potentially associated
1945 # with that branch.
1946 if self.GetIssue():
tandrii5d48c322016-08-18 23:19:371947 self._rietveld_server = gclient_utils.UpgradeToHttps(
1948 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
[email protected]aa5ced12016-03-29 09:41:141949 if not self._rietveld_server:
1950 self._rietveld_server = settings.GetDefaultServerUrl()
1951 return self._rietveld_server
1952
Andrii Shyshkalov2f8e9242017-01-23 18:20:191953 def EnsureAuthenticated(self, force, refresh=False):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:581954 # No checks for Rietveld because we are deprecating Rietveld.
1955 pass
[email protected]9e6c3a52016-04-12 14:13:081956
Andrii Shyshkalovbb86fbb2017-03-24 13:59:281957 def EnsureCanUploadPatchset(self, force):
1958 # No checks for Rietveld because we are deprecating Rietveld.
1959 pass
1960
Kenneth Russell61e2ed42017-02-15 19:47:131961 def FetchDescription(self, force=False):
Andrii Shyshkalov642641d2018-10-16 05:54:411962 raise NotImplementedError()
[email protected]aa5ced12016-03-29 09:41:141963
1964 def GetMostRecentPatchset(self):
Andrii Shyshkalov642641d2018-10-16 05:54:411965 raise NotImplementedError()
[email protected]aa5ced12016-03-29 09:41:141966
[email protected]aa5ced12016-03-29 09:41:141967 def GetIssueProperties(self):
Andrii Shyshkalov642641d2018-10-16 05:54:411968 raise NotImplementedError()
[email protected]aa5ced12016-03-29 09:41:141969
tandriie113dfd2016-10-11 17:20:121970 def CannotTriggerTryJobReason(self):
Andrii Shyshkalov03da1502018-10-15 03:42:341971 raise NotImplementedError()
tandriie113dfd2016-10-11 17:20:121972
Quinten Yearsley0c62da92017-05-31 20:39:421973 def GetTryJobProperties(self, patchset=None):
Andrii Shyshkalov03da1502018-10-15 03:42:341974 raise NotImplementedError()
tandrii8c5a3532016-11-04 14:52:021975
tandriide281ae2016-10-12 13:02:301976 def GetIssueOwner(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:581977 raise NotImplementedError()
tandriide281ae2016-10-12 13:02:301978
Edward Lemur707d70b2018-02-06 23:50:141979 def GetReviewers(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:581980 raise NotImplementedError()
Edward Lemur707d70b2018-02-06 23:50:141981
Aaron Gable636b13f2017-07-14 17:42:481982 def AddComment(self, message, publish=None):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:581983 raise NotImplementedError()
[email protected]aa5ced12016-03-29 09:41:141984
Andrii Shyshkalov51bdf8c2018-10-18 01:07:581985 def GetCommentsSummary(self, readable=True):
1986 raise NotImplementedError()
Andrii Shyshkalovd8aa49f2017-03-17 15:05:491987
[email protected]b99fbd92014-09-11 17:29:281988 def GetStatus(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:581989 print(
1990 'WARNING! Rietveld is no longer supported.\n'
1991 '\n'
1992 'If you have old branches in your checkout, please archive/delete them.\n'
1993 ' $ git cl archive --help\n'
1994 '\n'
1995 'See also PSA https://groups.google.com/a/chromium.org/'
1996 'forum/#!topic/infra-dev/2DIVzM2wseo\n')
1997 return 'rietveld-not-supported'
[email protected]b99fbd92014-09-11 17:29:281998
dsansomee2d6fd92016-09-08 07:10:471999 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:582000 raise NotImplementedError()
[email protected]b021b322013-04-08 17:57:292001
[email protected]cc51cd02010-12-23 00:48:392002 def CloseIssue(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:582003 raise NotImplementedError()
[email protected]cc51cd02010-12-23 00:48:392004
[email protected]27bb3872011-05-30 20:33:192005 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 10:50:292006 return self.SetFlags({flag: value})
2007
2008 def SetFlags(self, flags):
2009 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 10:50:292010 """
phajdan.jr68598232016-08-10 10:28:282011 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
[email protected]27bb3872011-05-30 20:33:192012 try:
tandrii4b233bd2016-07-06 10:50:292013 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 10:28:282014 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 15:33:572015 except urllib2.HTTPError as e:
[email protected]27bb3872011-05-30 20:33:192016 if e.code == 404:
2017 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2018 if e.code == 403:
2019 DieWithError(
2020 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 10:28:282021 'match?') % (self.GetIssue(), patchset))
[email protected]27bb3872011-05-30 20:33:192022 raise
[email protected]cc51cd02010-12-23 00:48:392023
[email protected]cab38e92011-04-09 00:30:512024 def RpcServer(self):
[email protected]cc51cd02010-12-23 00:48:392025 """Returns an upload.RpcServer() to access this review's rietveld instance.
2026 """
[email protected]e77ebbf2011-03-29 20:35:382027 if not self._rpc_server:
[email protected]4bac4b52012-11-27 20:33:522028 self._rpc_server = rietveld.CachingRietveld(
[email protected]aa5ced12016-03-29 09:41:142029 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 12:57:522030 self._auth_config)
[email protected]e77ebbf2011-03-29 20:35:382031 return self._rpc_server
[email protected]cc51cd02010-12-23 00:48:392032
[email protected]5df290f2016-04-11 16:12:292033 @classmethod
tandrii5d48c322016-08-18 23:19:372034 def IssueConfigKey(cls):
[email protected]5df290f2016-04-11 16:12:292035 return 'rietveldissue'
[email protected]cc51cd02010-12-23 00:48:392036
tandrii5d48c322016-08-18 23:19:372037 @classmethod
2038 def PatchsetConfigKey(cls):
2039 return 'rietveldpatchset'
[email protected]cc51cd02010-12-23 00:48:392040
tandrii5d48c322016-08-18 23:19:372041 @classmethod
2042 def CodereviewServerConfigKey(cls):
2043 return 'rietveldserver'
[email protected]9b7fd712016-06-01 13:45:202044
Ravi Mistry31e7d562018-04-02 16:53:572045 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2046 raise NotImplementedError()
2047
[email protected]fa330e82016-04-13 17:09:522048 def SetCQState(self, new_state):
2049 props = self.GetIssueProperties()
2050 if props.get('private'):
2051 DieWithError('Cannot set-commit on private issue')
2052
2053 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 15:22:562054 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
[email protected]fa330e82016-04-13 17:09:522055 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 10:50:292056 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
[email protected]fa330e82016-04-13 17:09:522057 else:
tandrii4b233bd2016-07-06 10:50:292058 assert new_state == _CQState.DRY_RUN
2059 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
[email protected]fa330e82016-04-13 17:09:522060
[email protected]f86c7d32016-04-01 19:27:302061 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 15:22:092062 directory, force):
[email protected]f86c7d32016-04-01 19:27:302063 # PatchIssue should never be called with a dirty tree. It is up to the
2064 # caller to check this, but just in case we assert here since the
2065 # consequences of the caller not checking this could be dire.
2066 assert(not git_common.is_dirty_git_tree('apply'))
2067 assert(parsed_issue_arg.valid)
2068 self._changelist.issue = parsed_issue_arg.issue
2069 if parsed_issue_arg.hostname:
2070 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2071
skobes6468b902016-10-24 15:45:102072 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2073 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2074 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
[email protected]f86c7d32016-04-01 19:27:302075 try:
skobes6468b902016-10-24 15:45:102076 scm_obj.apply_patch(patchset_object)
2077 except Exception as e:
2078 print(str(e))
[email protected]f86c7d32016-04-01 19:27:302079 return 1
2080
2081 # If we had an issue, commit the current state and register the issue.
2082 if not nocommit:
Aaron Gabled343c632017-03-15 18:02:262083 self.SetIssue(self.GetIssue())
2084 self.SetPatchset(patchset)
[email protected]f86c7d32016-04-01 19:27:302085 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2086 'patch from issue %(i)s at patchset '
2087 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2088 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 16:17:492089 print('Committed patch locally.')
[email protected]f86c7d32016-04-01 19:27:302090 else:
vapiera7fbd5a2016-06-16 16:17:492091 print('Patch applied to index.')
[email protected]f86c7d32016-04-01 19:27:302092 return 0
2093
2094 @staticmethod
2095 def ParseIssueURL(parsed_url):
2096 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2097 return None
wychen3c1c1722016-08-04 18:46:362098 # Rietveld patch: https://domain/<number>/#ps<patchset>
2099 match = re.match(r'/(\d+)/$', parsed_url.path)
2100 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2101 if match and match2:
skobes6468b902016-10-24 15:45:102102 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 18:46:362103 issue=int(match.group(1)),
2104 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 14:10:212105 hostname=parsed_url.netloc,
2106 codereview='rietveld')
[email protected]f86c7d32016-04-01 19:27:302107 # Typical url: https://domain/<issue_number>[/[other]]
2108 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2109 if match:
skobes6468b902016-10-24 15:45:102110 return _ParsedIssueNumberArgument(
[email protected]f86c7d32016-04-01 19:27:302111 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 14:10:212112 hostname=parsed_url.netloc,
2113 codereview='rietveld')
[email protected]f86c7d32016-04-01 19:27:302114 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2115 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2116 if match:
skobes6468b902016-10-24 15:45:102117 return _ParsedIssueNumberArgument(
[email protected]f86c7d32016-04-01 19:27:302118 issue=int(match.group(1)),
2119 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 14:10:212120 hostname=parsed_url.netloc,
2121 codereview='rietveld')
[email protected]f86c7d32016-04-01 19:27:302122 return None
2123
Andrii Shyshkalov550e9242017-04-12 15:14:492124 def CMDUploadChange(self, options, args, custom_cl_base, change):
[email protected]aa6235b2016-04-11 21:35:292125 """Upload the patch to Rietveld."""
Andrii Shyshkalov9f274432018-10-15 16:40:232126 raise NotImplementedError
[email protected]aa6235b2016-04-11 21:35:292127
Andrii Shyshkalov18975322017-01-25 15:44:132128
[email protected]aa5ced12016-03-29 09:41:142129class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 08:38:182130 def __init__(self, changelist, auth_config=None, codereview_host=None):
[email protected]aa5ced12016-03-29 09:41:142131 # auth_config is Rietveld thing, kept here to preserve interface only.
2132 super(_GerritChangelistImpl, self).__init__(changelist)
2133 self._change_id = None
[email protected]fe30f182016-04-13 12:15:042134 # Lazily cached values.
[email protected]fe30f182016-04-13 12:15:042135 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 08:38:182136 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 15:50:572137 # Map from change number (issue) to its detail cache.
2138 self._detail_cache = {}
[email protected]aa5ced12016-03-29 09:41:142139
Andrii Shyshkalov8039be72017-01-26 08:38:182140 if codereview_host is not None:
2141 assert not codereview_host.startswith('https://'), codereview_host
2142 self._gerrit_host = codereview_host
2143 self._gerrit_server = 'https://%s' % codereview_host
2144
[email protected]aa5ced12016-03-29 09:41:142145 def _GetGerritHost(self):
2146 # Lazy load of configs.
2147 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 09:52:482148 if self._gerrit_host and '.' not in self._gerrit_host:
2149 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2150 # This happens for internal stuff http://crbug.com/614312.
2151 parsed = urlparse.urlparse(self.GetRemoteUrl())
2152 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 20:39:422153 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 09:52:482154 ' Your current remote is: %s' % self.GetRemoteUrl())
2155 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2156 self._gerrit_server = 'https://%s' % self._gerrit_host
[email protected]aa5ced12016-03-29 09:41:142157 return self._gerrit_host
2158
[email protected]fe30f182016-04-13 12:15:042159 def _GetGitHost(self):
2160 """Returns git host to be used when uploading change to Gerrit."""
2161 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2162
[email protected]aa5ced12016-03-29 09:41:142163 def GetCodereviewServer(self):
2164 if not self._gerrit_server:
2165 # If we're on a branch then get the server potentially associated
2166 # with that branch.
2167 if self.GetIssue():
tandrii5d48c322016-08-18 23:19:372168 self._gerrit_server = self._GitGetBranchConfigValue(
2169 self.CodereviewServerConfigKey())
2170 if self._gerrit_server:
2171 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
[email protected]aa5ced12016-03-29 09:41:142172 if not self._gerrit_server:
2173 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2174 # has "-review" suffix for lowest level subdomain.
[email protected]fe30f182016-04-13 12:15:042175 parts = self._GetGitHost().split('.')
[email protected]aa5ced12016-03-29 09:41:142176 parts[0] = parts[0] + '-review'
2177 self._gerrit_host = '.'.join(parts)
2178 self._gerrit_server = 'https://%s' % self._gerrit_host
2179 return self._gerrit_server
2180
Andrii Shyshkalov2d0e03c2018-08-25 04:18:092181 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:582182 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:092183 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:582184 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:092185 logging.warn('can\'t detect Gerrit project.')
2186 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:582187 project = urlparse.urlparse(remote_url).path.strip('/')
2188 if project.endswith('.git'):
2189 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:372190 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
2191 # 'a/' prefix, because 'a/' prefix is used to force authentication in
2192 # gitiles/git-over-https protocol. E.g.,
2193 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
2194 # as
2195 # https://chromium.googlesource.com/v8/v8
2196 if project.startswith('a/'):
2197 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:582198 return project
2199
Andrii Shyshkalovd06cc782018-08-23 17:24:192200 def _GerritChangeIdentifier(self):
2201 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2202
2203 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:092204 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:192205 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:092206 project = self._GetGerritProject()
2207 if project:
2208 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2209 # Fall back on still unique, but less efficient change number.
2210 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:192211
[email protected]5df290f2016-04-11 16:12:292212 @classmethod
tandrii5d48c322016-08-18 23:19:372213 def IssueConfigKey(cls):
[email protected]5df290f2016-04-11 16:12:292214 return 'gerritissue'
[email protected]aa5ced12016-03-29 09:41:142215
tandrii5d48c322016-08-18 23:19:372216 @classmethod
2217 def PatchsetConfigKey(cls):
2218 return 'gerritpatchset'
2219
2220 @classmethod
2221 def CodereviewServerConfigKey(cls):
2222 return 'gerritserver'
2223
Andrii Shyshkalov2f8e9242017-01-23 18:20:192224 def EnsureAuthenticated(self, force, refresh=None):
[email protected]9e6c3a52016-04-12 14:13:082225 """Best effort check that user is authenticated with Gerrit server."""
[email protected]28253532016-04-14 13:46:562226 if settings.GetGerritSkipEnsureAuthenticated():
2227 # For projects with unusual authentication schemes.
2228 # See http://crbug.com/603378.
2229 return
Vadim Shtayurab250ec12018-10-04 00:21:082230
2231 # Check presence of cookies only if using cookies-based auth method.
2232 cookie_auth = gerrit_util.Authenticator.get()
2233 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
[email protected]fe30f182016-04-13 12:15:042234 return
Vadim Shtayurab250ec12018-10-04 00:21:082235
2236 # Lazy-loader to identify Gerrit and Git hosts.
[email protected]fe30f182016-04-13 12:15:042237 self.GetCodereviewServer()
2238 git_host = self._GetGitHost()
2239 assert self._gerrit_server and self._gerrit_host
[email protected]fe30f182016-04-13 12:15:042240
2241 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2242 git_auth = cookie_auth.get_auth_header(git_host)
2243 if gerrit_auth and git_auth:
2244 if gerrit_auth == git_auth:
2245 return
Andrii Shyshkalov354e1d22017-06-09 17:31:332246 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
[email protected]fe30f182016-04-13 12:15:042247 print((
Quinten Yearsley0c62da92017-05-31 20:39:422248 'WARNING: You have different credentials for Gerrit and git hosts:\n'
[email protected]fe30f182016-04-13 12:15:042249 ' %s\n'
2250 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 15:19:592251 ' Consider running the following command:\n'
2252 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 17:31:332253 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 13:46:532254 ' %s') %
Andrii Shyshkalov51acef92017-04-11 15:19:592255 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 17:31:332256 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
[email protected]fe30f182016-04-13 12:15:042257 cookie_auth.get_new_password_message(git_host)))
2258 if not force:
Andrii Shyshkalovabc26ac2017-03-14 13:49:382259 confirm_or_exit('If you know what you are doing', action='continue')
[email protected]fe30f182016-04-13 12:15:042260 return
2261 else:
2262 missing = (
Anna Henningsen4e891442017-07-06 19:40:582263 ([] if gerrit_auth else [self._gerrit_host]) +
2264 ([] if git_auth else [git_host]))
[email protected]fe30f182016-04-13 12:15:042265 DieWithError('Credentials for the following hosts are required:\n'
2266 ' %s\n'
2267 'These are read from %s (or legacy %s)\n'
2268 '%s' % (
2269 '\n '.join(missing),
2270 cookie_auth.get_gitcookies_path(),
2271 cookie_auth.get_netrc_path(),
2272 cookie_auth.get_new_password_message(git_host)))
2273
Andrii Shyshkalovbb86fbb2017-03-24 13:59:282274 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 16:46:442275 if not self.GetIssue():
2276 return
2277
2278 # Warm change details cache now to avoid RPCs later, reducing latency for
2279 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 13:59:282280 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:172281 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 16:46:442282
2283 status = self._GetChangeDetail()['status']
2284 if status in ('MERGED', 'ABANDONED'):
2285 DieWithError('Change %s has been %s, new uploads are not allowed' %
2286 (self.GetIssueURL(),
2287 'submitted' if status == 'MERGED' else 'abandoned'))
2288
Vadim Shtayurab250ec12018-10-04 00:21:082289 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2290 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2291 # Apparently this check is not very important? Otherwise get_auth_email
2292 # could have been added to other implementations of Authenticator.
2293 cookies_auth = gerrit_util.Authenticator.get()
2294 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 13:59:282295 return
Vadim Shtayurab250ec12018-10-04 00:21:082296
2297 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 13:59:282298 if self.GetIssueOwner() == cookies_user:
2299 return
2300 logging.debug('change %s owner is %s, cookies user is %s',
2301 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 20:39:422302 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 13:59:282303 # so ask what Gerrit thinks of this user.
2304 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2305 if details['email'] == self.GetIssueOwner():
2306 return
2307 if not force:
Quinten Yearsley0c62da92017-05-31 20:39:422308 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 13:59:282309 'as %s.\n'
2310 'Uploading may fail due to lack of permissions.' %
2311 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2312 confirm_or_exit(action='upload')
2313
[email protected]9b7fd712016-06-01 13:45:202314 def _PostUnsetIssueProperties(self):
2315 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 23:19:372316 return ['gerritsquashhash']
[email protected]9b7fd712016-06-01 13:45:202317
[email protected]37b07a72016-04-29 16:42:282318 def GetGerritObjForPresubmit(self):
2319 return presubmit_support.GerritAccessor(self._GetGerritHost())
2320
[email protected]aa5ced12016-03-29 09:41:142321 def GetStatus(self):
[email protected]013a2802016-03-29 09:52:332322 """Apply a rough heuristic to give a simple summary of an issue's review
2323 or CQ status, assuming adherence to a common workflow.
2324
2325 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 21:36:332326 * 'error' - error from review tool (including deleted issues)
2327 * 'unsent' - no reviewers added
2328 * 'waiting' - waiting for review
2329 * 'reply' - waiting for uploader to reply to review
2330 * 'lgtm' - Code-Review label has been set
2331 * 'commit' - in the commit queue
2332 * 'closed' - successfully submitted or abandoned
[email protected]013a2802016-03-29 09:52:332333 """
2334 if not self.GetIssue():
2335 return None
2336
2337 try:
Aaron Gable9ab38c62017-04-06 21:36:332338 data = self._GetChangeDetail([
2339 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 23:14:382340 except (httplib.HTTPException, GerritChangeNotExists):
[email protected]013a2802016-03-29 09:52:332341 return 'error'
2342
[email protected]5e1bf382016-05-17 08:43:242343 if data['status'] in ('ABANDONED', 'MERGED'):
[email protected]013a2802016-03-29 09:52:332344 return 'closed'
2345
Aaron Gable9ab38c62017-04-06 21:36:332346 if data['labels'].get('Commit-Queue', {}).get('approved'):
2347 # The section will have an "approved" subsection if anyone has voted
2348 # the maximum value on the label.
2349 return 'commit'
[email protected]013a2802016-03-29 09:52:332350
Aaron Gable9ab38c62017-04-06 21:36:332351 if data['labels'].get('Code-Review', {}).get('approved'):
2352 return 'lgtm'
[email protected]013a2802016-03-29 09:52:332353
2354 if not data.get('reviewers', {}).get('REVIEWER', []):
2355 return 'unsent'
2356
Andrii Shyshkalov33e88a42017-01-27 13:45:302357 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 21:36:332358 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2359 last_message_author = messages.pop().get('author', {})
2360 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 13:45:302361 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2362 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 21:36:332363 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 13:45:302364 continue
Aaron Gable9ab38c62017-04-06 21:36:332365 if last_message_author.get('_account_id') == owner:
2366 # Most recent message was by owner.
2367 return 'waiting'
2368 else:
[email protected]013a2802016-03-29 09:52:332369 # Some reply from non-owner.
2370 return 'reply'
Aaron Gable9ab38c62017-04-06 21:36:332371
2372 # Somehow there are no messages even though there are reviewers.
2373 return 'unsent'
[email protected]aa5ced12016-03-29 09:41:142374
2375 def GetMostRecentPatchset(self):
[email protected]013a2802016-03-29 09:52:332376 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 20:41:462377 patchset = data['revisions'][data['current_revision']]['_number']
2378 self.SetPatchset(patchset)
2379 return patchset
[email protected]aa5ced12016-03-29 09:41:142380
Kenneth Russell61e2ed42017-02-15 19:47:132381 def FetchDescription(self, force=False):
2382 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2383 no_cache=force)
[email protected]2d3da632016-04-25 19:23:272384 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 16:41:222385 return data['revisions'][current_rev]['commit']['message']
[email protected]aa5ced12016-03-29 09:41:142386
dsansomee2d6fd92016-09-08 07:10:472387 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:062388 if gerrit_util.HasPendingChangeEdit(
2389 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 07:10:472390 if not force:
Andrii Shyshkalovabc26ac2017-03-14 13:49:382391 confirm_or_exit(
dsansomee2d6fd92016-09-08 07:10:472392 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 13:49:382393 'unpublished edit. Either publish the edit in the Gerrit web UI '
2394 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 07:10:472395
Andrii Shyshkalov889677c2018-08-28 20:43:062396 gerrit_util.DeletePendingChangeEdit(
2397 self._GetGerritHost(), self._GerritChangeIdentifier())
2398 gerrit_util.SetCommitMessage(
2399 self._GetGerritHost(), self._GerritChangeIdentifier(),
2400 description, notify='NONE')
[email protected]aa5ced12016-03-29 09:41:142401
Aaron Gable636b13f2017-07-14 17:42:482402 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:062403 gerrit_util.SetReview(
2404 self._GetGerritHost(), self._GerritChangeIdentifier(),
2405 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-15 23:24:372406
Aaron Gable0ffdf2d2017-06-05 20:01:172407 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 15:14:592408 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 20:01:172409 messages = self._GetChangeDetail(
2410 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2411 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:062412 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 20:01:172413
2414 # Build dictionary of file comments for easy access and sorting later.
2415 # {author+date: {path: {patchset: {line: url+message}}}}
2416 comments = collections.defaultdict(
2417 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2418 for path, line_comments in file_comments.iteritems():
2419 for comment in line_comments:
2420 if comment.get('tag', '').startswith('autogenerated'):
2421 continue
2422 key = (comment['author']['email'], comment['updated'])
2423 if comment.get('side', 'REVISION') == 'PARENT':
2424 patchset = 'Base'
2425 else:
2426 patchset = 'PS%d' % comment['patch_set']
2427 line = comment.get('line', 0)
2428 url = ('https://%s/c/%s/%s/%s#%s%s' %
2429 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2430 'b' if comment.get('side') == 'PARENT' else '',
2431 str(line) if line else ''))
2432 comments[key][path][patchset][line] = (url, comment['message'])
2433
Andrii Shyshkalov5a0cf202017-03-17 15:14:592434 summary = []
Aaron Gable0ffdf2d2017-06-05 20:01:172435 for msg in messages:
2436 # Don't bother showing autogenerated messages.
2437 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2438 continue
Andrii Shyshkalov5a0cf202017-03-17 15:14:592439 # Gerrit spits out nanoseconds.
2440 assert len(msg['date'].split('.')[-1]) == 9
2441 date = datetime.datetime.strptime(msg['date'][:-3],
2442 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 20:01:172443 message = msg['message']
2444 key = (msg['author']['email'], msg['date'])
2445 if key in comments:
2446 message += '\n'
2447 for path, patchsets in sorted(comments.get(key, {}).items()):
2448 if readable:
2449 message += '\n%s' % path
2450 for patchset, lines in sorted(patchsets.items()):
2451 for line, (url, content) in sorted(lines.items()):
2452 if line:
2453 line_str = 'Line %d' % line
2454 path_str = '%s:%d:' % (path, line)
2455 else:
2456 line_str = 'File comment'
2457 path_str = '%s:0:' % path
2458 if readable:
2459 message += '\n %s, %s: %s' % (patchset, line_str, url)
2460 message += '\n %s\n' % content
2461 else:
2462 message += '\n%s ' % path_str
2463 message += '\n%s\n' % content
2464
Andrii Shyshkalov5a0cf202017-03-17 15:14:592465 summary.append(_CommentSummary(
2466 date=date,
Aaron Gable0ffdf2d2017-06-05 20:01:172467 message=message,
Andrii Shyshkalov5a0cf202017-03-17 15:14:592468 sender=msg['author']['email'],
2469 # These could be inferred from the text messages and correlated with
2470 # Code-Review label maximum, however this is not reliable.
2471 # Leaving as is until the need arises.
2472 approval=False,
2473 disapproval=False,
2474 ))
2475 return summary
Andrii Shyshkalovd8aa49f2017-03-17 15:05:492476
[email protected]aa5ced12016-03-29 09:41:142477 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:062478 gerrit_util.AbandonChange(
2479 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
[email protected]aa5ced12016-03-29 09:41:142480
[email protected]d68b62b2016-03-31 16:09:292481 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:062482 gerrit_util.SubmitChange(
2483 self._GetGerritHost(), self._GerritChangeIdentifier(),
2484 wait_for_merge=wait_for_merge)
[email protected]cc51cd02010-12-23 00:48:392485
Andrii Shyshkalovb7214602018-08-22 23:20:262486 def _GetChangeDetail(self, options=None, no_cache=False):
2487 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 15:50:572488
2489 If fresh data is needed, set no_cache=True which will clear cache and
2490 thus new data will be fetched from Gerrit.
2491 """
[email protected]aa6235b2016-04-11 21:35:292492 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:302493 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 15:50:572494
Andrii Shyshkalov8039be72017-01-26 08:38:182495 # Optimization to avoid multiple RPCs:
2496 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2497 'CURRENT_COMMIT' not in options):
2498 options.append('CURRENT_COMMIT')
2499
Andrii Shyshkalov258e0a62017-01-24 15:50:572500 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:302501 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 15:50:572502 options = [o.upper() for o in options]
2503
2504 # Check in cache first unless no_cache is True.
2505 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:302506 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 15:50:572507 else:
2508 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:302509 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 15:50:572510 # Assumption: data fetched before with extra options is suitable
2511 # for return for a smaller set of options.
2512 # For example, if we cached data for
2513 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2514 # and request is for options=[CURRENT_REVISION],
2515 # THEN we can return prior cached data.
2516 if options_set.issubset(cached_options_set):
2517 return data
2518
Andrii Shyshkalovc6c8b4c2016-11-09 19:51:202519 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:302520 data = gerrit_util.GetChangeDetail(
2521 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 19:51:202522 except gerrit_util.GerritError as e:
2523 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:302524 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 19:51:202525 raise
Andrii Shyshkalov258e0a62017-01-24 15:50:572526
Andrii Shyshkalov03e0ed22018-08-28 19:39:302527 self._detail_cache.setdefault(cache_key, []).append(
2528 (frozenset(options), data))
tandriic2405f52016-10-10 15:13:152529 return data
[email protected]013a2802016-03-29 09:52:332530
Andrii Shyshkalovcc5f17e2018-08-22 23:35:592531 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:312532 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 21:49:052533 try:
Andrii Shyshkalove2633162018-08-27 23:50:312534 data = gerrit_util.GetChangeCommit(
2535 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 21:49:052536 except gerrit_util.GerritError as e:
2537 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:312538 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 21:49:052539 raise
agable32978d92016-11-01 19:55:022540 return data
2541
Olivier Robin75ee7252018-04-13 08:02:562542 def CMDLand(self, force, bypass_hooks, verbose, parallel):
[email protected]d68b62b2016-03-31 16:09:292543 if git_common.is_dirty_git_tree('land'):
2544 return 1
tandriid60367b2016-06-22 12:25:122545 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2546 if u'Commit-Queue' in detail.get('labels', {}):
2547 if not force:
Andrii Shyshkalovabc26ac2017-03-14 13:49:382548 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2549 'which can test and land changes for you. '
2550 'Are you sure you wish to bypass it?\n',
2551 action='bypass CQ')
tandriid60367b2016-06-22 12:25:122552
[email protected]d68b62b2016-03-31 16:09:292553 differs = True
tandriic4344b52016-08-29 13:04:542554 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
[email protected]d68b62b2016-03-31 16:09:292555 # Note: git diff outputs nothing if there is no diff.
2556 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 20:39:422557 print('WARNING: Some changes from local branch haven\'t been uploaded.')
[email protected]d68b62b2016-03-31 16:09:292558 else:
[email protected]d68b62b2016-03-31 16:09:292559 if detail['current_revision'] == last_upload:
2560 differs = False
2561 else:
Quinten Yearsley0c62da92017-05-31 20:39:422562 print('WARNING: Local branch contents differ from latest uploaded '
2563 'patchset.')
[email protected]d68b62b2016-03-31 16:09:292564 if differs:
2565 if not force:
Andrii Shyshkalovabc26ac2017-03-14 13:49:382566 confirm_or_exit(
2567 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2568 action='submit')
Quinten Yearsley0c62da92017-05-31 20:39:422569 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
[email protected]d68b62b2016-03-31 16:09:292570 elif not bypass_hooks:
2571 hook_results = self.RunHook(
2572 committing=True,
2573 may_prompt=not force,
2574 verbose=verbose,
Olivier Robin75ee7252018-04-13 08:02:562575 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2576 parallel=parallel)
[email protected]d68b62b2016-03-31 16:09:292577 if not hook_results.should_continue():
2578 return 1
2579
2580 self.SubmitIssue(wait_for_merge=True)
2581 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 19:55:022582 links = self._GetChangeCommit().get('web_links', [])
2583 for link in links:
Aaron Gable02cdbb42016-12-14 00:24:252584 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 20:39:422585 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 19:55:022586 break
[email protected]d68b62b2016-03-31 16:09:292587 return 0
2588
[email protected]f86c7d32016-04-01 19:27:302589 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 15:22:092590 directory, force):
[email protected]f86c7d32016-04-01 19:27:302591 assert not reject
[email protected]f86c7d32016-04-01 19:27:302592 assert not directory
2593 assert parsed_issue_arg.valid
2594
2595 self._changelist.issue = parsed_issue_arg.issue
2596
2597 if parsed_issue_arg.hostname:
2598 self._gerrit_host = parsed_issue_arg.hostname
2599 self._gerrit_server = 'https://%s' % self._gerrit_host
2600
tandriic2405f52016-10-10 15:13:152601 try:
2602 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 23:14:382603 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 15:13:152604 DieWithError(str(e))
[email protected]f86c7d32016-04-01 19:27:302605
2606 if not parsed_issue_arg.patchset:
2607 # Use current revision by default.
2608 revision_info = detail['revisions'][detail['current_revision']]
2609 patchset = int(revision_info['_number'])
2610 else:
2611 patchset = parsed_issue_arg.patchset
2612 for revision_info in detail['revisions'].itervalues():
2613 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2614 break
2615 else:
Aaron Gablea45ee112016-11-22 23:14:382616 DieWithError('Couldn\'t find patchset %i in change %i' %
[email protected]f86c7d32016-04-01 19:27:302617 (parsed_issue_arg.patchset, self.GetIssue()))
2618
Aaron Gable697a91b2018-01-19 23:20:152619 remote_url = self._changelist.GetRemoteUrl()
2620 if remote_url.endswith('.git'):
2621 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:092622 remote_url = remote_url.rstrip('/')
2623
[email protected]f86c7d32016-04-01 19:27:302624 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:092625 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 23:20:152626
2627 if remote_url != fetch_info['url']:
2628 DieWithError('Trying to patch a change from %s but this repo appears '
2629 'to be %s.' % (fetch_info['url'], remote_url))
2630
[email protected]f86c7d32016-04-01 19:27:302631 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 17:50:032632
Aaron Gable62619a32017-06-16 15:22:092633 if force:
2634 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2635 print('Checked out commit for change %i patchset %i locally' %
2636 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 22:17:532637 elif nocommit:
2638 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2639 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 15:22:092640 else:
Aaron Gable9387b4f2017-06-08 17:50:032641 RunGit(['cherry-pick', 'FETCH_HEAD'])
2642 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 15:22:092643 (parsed_issue_arg.issue, patchset))
2644 print('Note: this created a local commit which does not have '
2645 'the same hash as the one uploaded for review. This will make '
2646 'uploading changes based on top of this branch difficult.\n'
2647 'If you want to do that, use "git cl patch --force" instead.')
2648
Stefan Zagerd08043c2017-10-12 19:07:022649 if self.GetBranch():
2650 self.SetIssue(parsed_issue_arg.issue)
2651 self.SetPatchset(patchset)
2652 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2653 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2654 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2655 else:
2656 print('WARNING: You are in detached HEAD state.\n'
2657 'The patch has been applied to your checkout, but you will not be '
2658 'able to upload a new patch set to the gerrit issue.\n'
2659 'Try using the \'-b\' option if you would like to work on a '
2660 'branch and/or upload a new patch set.')
2661
[email protected]f86c7d32016-04-01 19:27:302662 return 0
2663
2664 @staticmethod
2665 def ParseIssueURL(parsed_url):
2666 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2667 return None
Aaron Gable01b91062017-08-25 00:48:402668 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2669 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
[email protected]f86c7d32016-04-01 19:27:302670 # Short urls like https://domain/<issue_number> can be used, but don't allow
2671 # specifying the patchset (you'd 404), but we allow that here.
2672 if parsed_url.path == '/':
2673 part = parsed_url.fragment
2674 else:
2675 part = parsed_url.path
Aaron Gable01b91062017-08-25 00:48:402676 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
[email protected]f86c7d32016-04-01 19:27:302677 if match:
2678 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-25 00:48:402679 issue=int(match.group(3)),
2680 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 14:10:212681 hostname=parsed_url.netloc,
2682 codereview='gerrit')
[email protected]f86c7d32016-04-01 19:27:302683 return None
2684
tandrii16e0b4e2016-06-07 17:34:282685 def _GerritCommitMsgHookCheck(self, offer_removal):
2686 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2687 if not os.path.exists(hook):
2688 return
2689 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2690 # custom developer made one.
2691 data = gclient_utils.FileRead(hook)
2692 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2693 return
Quinten Yearsley0c62da92017-05-31 20:39:422694 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 16:18:402695 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 17:34:282696 'and may interfere with it in subtle ways.\n'
2697 'We recommend you remove the commit-msg hook.')
2698 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 13:49:382699 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 17:34:282700 gclient_utils.rm_file_or_tree(hook)
2701 print('Gerrit commit-msg hook removed.')
2702 else:
2703 print('OK, will keep Gerrit commit-msg hook in place.')
2704
Andrii Shyshkalov550e9242017-04-12 15:14:492705 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
[email protected]aa6235b2016-04-11 21:35:292706 """Upload the current branch to Gerrit."""
[email protected]9e6c3a52016-04-12 14:13:082707 if options.squash and options.no_squash:
2708 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 09:01:532709
2710 if not options.squash and not options.no_squash:
2711 # Load default for user, repo, squash=true, in this order.
2712 options.squash = settings.GetSquashGerritUploads()
2713 elif options.no_squash:
2714 options.squash = False
tandrii26f3e4e2016-06-10 15:37:042715
[email protected]aa6235b2016-04-11 21:35:292716 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 20:23:572717 branch = GetTargetRef(remote, remote_branch, options.target_branch)
[email protected]aa6235b2016-04-11 21:35:292718
Aaron Gableb56ad332017-01-06 23:24:312719 # This may be None; default fallback value is determined in logic below.
2720 title = options.title
2721
Dominic Battre7d1c4842017-10-27 07:17:282722 # Extract bug number from branch name.
2723 bug = options.bug
2724 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2725 if not bug and match:
2726 bug = match.group(1)
2727
[email protected]aa6235b2016-04-11 21:35:292728 if options.squash:
tandrii16e0b4e2016-06-07 17:34:282729 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
[email protected]aa6235b2016-04-11 21:35:292730 if self.GetIssue():
2731 # Try to get the message from a previous upload.
2732 message = self.GetDescription()
2733 if not message:
2734 DieWithError(
Aaron Gablea45ee112016-11-22 23:14:382735 'failed to fetch description from current Gerrit change %d\n'
[email protected]aa6235b2016-04-11 21:35:292736 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 23:24:312737 if not title:
Andrii Shyshkalov2d8a2072017-04-10 13:02:182738 if options.message:
Aaron Gable7303dcb2017-06-07 21:17:322739 # When uploading a subsequent patchset, -m|--message is taken
2740 # as the patchset title if --title was not provided.
2741 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 13:02:182742 else:
2743 default_title = RunGit(
2744 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 21:17:322745 if options.force:
2746 title = default_title
2747 else:
2748 title = ask_for_data(
2749 'Title for patchset [%s]: ' % default_title) or default_title
[email protected]aa6235b2016-04-11 21:35:292750 change_id = self._GetChangeDetail()['change_id']
2751 while True:
2752 footer_change_ids = git_footers.get_footer_change_id(message)
2753 if footer_change_ids == [change_id]:
2754 break
2755 if not footer_change_ids:
2756 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 20:39:422757 print('WARNING: appended missing Change-Id to change description.')
[email protected]aa6235b2016-04-11 21:35:292758 continue
2759 # There is already a valid footer but with different or several ids.
2760 # Doing this automatically is non-trivial as we don't want to lose
2761 # existing other footers, yet we want to append just 1 desired
2762 # Change-Id. Thus, just create a new footer, but let user verify the
2763 # new description.
2764 message = '%s\n\nChange-Id: %s' % (message, change_id)
2765 print(
Aaron Gablea45ee112016-11-22 23:14:382766 'WARNING: change %s has Change-Id footer(s):\n'
[email protected]aa6235b2016-04-11 21:35:292767 ' %s\n'
Aaron Gablea45ee112016-11-22 23:14:382768 'but change has Change-Id %s, according to Gerrit.\n'
[email protected]aa6235b2016-04-11 21:35:292769 'Please, check the proposed correction to the description, '
2770 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2771 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2772 change_id))
Andrii Shyshkalovabc26ac2017-03-14 13:49:382773 confirm_or_exit(action='edit')
[email protected]aa6235b2016-04-11 21:35:292774 if not options.force:
2775 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 07:17:282776 change_desc.prompt(bug=bug)
[email protected]aa6235b2016-04-11 21:35:292777 message = change_desc.description
2778 if not message:
2779 DieWithError("Description is empty. Aborting...")
2780 # Continue the while loop.
2781 # Sanity check of this code - we should end up with proper message
2782 # footer.
2783 assert [change_id] == git_footers.get_footer_change_id(message)
2784 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 23:24:312785 else: # if not self.GetIssue()
2786 if options.message:
2787 message = options.message
2788 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:212789 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 23:24:312790 if options.title:
2791 message = options.title + '\n\n' + message
2792 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 19:27:262793
[email protected]aa6235b2016-04-11 21:35:292794 if not options.force:
Dominic Battre7d1c4842017-10-27 07:17:282795 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 23:24:312796 # On first upload, patchset title is always this string, while
2797 # --title flag gets converted to first line of message.
2798 title = 'Initial upload'
[email protected]aa6235b2016-04-11 21:35:292799 if not change_desc.description:
2800 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 19:27:262801 change_ids = git_footers.get_footer_change_id(change_desc.description)
[email protected]aa6235b2016-04-11 21:35:292802 if len(change_ids) > 1:
2803 DieWithError('too many Change-Id footers, at most 1 allowed.')
2804 if not change_ids:
2805 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 19:27:262806 change_desc.set_description(git_footers.add_footer_change_id(
2807 change_desc.description,
2808 GenerateGerritChangeId(change_desc.description)))
2809 change_ids = git_footers.get_footer_change_id(change_desc.description)
[email protected]aa6235b2016-04-11 21:35:292810 assert len(change_ids) == 1
2811 change_id = change_ids[0]
2812
Robert Iannuccidb02dd02017-04-19 19:18:202813 if options.reviewers or options.tbrs or options.add_owners_to:
2814 change_desc.update_reviewers(options.reviewers, options.tbrs,
2815 options.add_owners_to, change)
2816
[email protected]aa6235b2016-04-11 21:35:292817 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 15:14:492818 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2819 options.force, change_desc)
[email protected]aa6235b2016-04-11 21:35:292820 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 18:31:072821 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2822 desc_tempfile.write(change_desc.description)
2823 desc_tempfile.close()
2824 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2825 '-F', desc_tempfile.name]).strip()
2826 os.remove(desc_tempfile.name)
[email protected]aa6235b2016-04-11 21:35:292827 else:
2828 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:212829 options.message or _create_description_from_log(git_diff_args))
[email protected]aa6235b2016-04-11 21:35:292830 if not change_desc.description:
2831 DieWithError("Description is empty. Aborting...")
2832
2833 if not git_footers.get_footer_change_id(change_desc.description):
2834 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 15:14:492835 change_desc.set_description(
2836 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 19:18:202837 if options.reviewers or options.tbrs or options.add_owners_to:
2838 change_desc.update_reviewers(options.reviewers, options.tbrs,
2839 options.add_owners_to, change)
[email protected]aa6235b2016-04-11 21:35:292840 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 15:14:492841 # For no-squash mode, we assume the remote called "origin" is the one we
2842 # want. It is not worthwhile to support different workflows for
2843 # no-squash mode.
2844 parent = 'origin/%s' % branch
[email protected]aa6235b2016-04-11 21:35:292845 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2846
2847 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:092848 SaveDescriptionBackup(change_desc)
[email protected]aa6235b2016-04-11 21:35:292849 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2850 ref_to_push)]).splitlines()
2851 if len(commits) > 1:
2852 print('WARNING: This will upload %d commits. Run the following command '
2853 'to see which commits will be uploaded: ' % len(commits))
2854 print('git log %s..%s' % (parent, ref_to_push))
2855 print('You can also use `git squash-branch` to squash these into a '
2856 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 13:49:382857 confirm_or_exit(action='upload')
[email protected]aa6235b2016-04-11 21:35:292858
Aaron Gable6dadfbf2017-05-09 21:27:582859 if options.reviewers or options.tbrs or options.add_owners_to:
2860 change_desc.update_reviewers(options.reviewers, options.tbrs,
2861 options.add_owners_to, change)
2862
Andrii Shyshkalov76988a82018-10-15 03:12:252863 reviewers = sorted(change_desc.get_reviewers())
2864 # Add cc's from the CC_LIST and --cc flag (if any).
2865 if not options.private and not options.no_autocc:
2866 cc = self.GetCCList().split(',')
2867 else:
2868 cc = []
2869 if options.cc:
2870 cc.extend(options.cc)
2871 cc = filter(None, [email.strip() for email in cc])
2872 if change_desc.get_cced():
2873 cc.extend(change_desc.get_cced())
Andrii Shyshkalovba7b0a42018-10-15 03:20:352874 valid_accounts = gerrit_util.ValidAccounts(
2875 self._GetGerritHost(), reviewers + cc)
2876 logging.debug('accounts %s are valid, %s invalid', sorted(valid_accounts),
2877 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:252878
[email protected]bf766ba2016-04-13 12:51:232879 # Extra options that can be specified at push time. Doc:
2880 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:202881 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 13:02:182882
Aaron Gable844cf292017-06-28 18:32:592883 # By default, new changes are started in WIP mode, and subsequent patchsets
2884 # don't send email. At any time, passing --send-mail will mark the change
2885 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 23:40:102886 if options.send_mail:
2887 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 18:32:592888 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 18:41:202889 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 18:22:062890 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 23:40:102891 else:
Nodir Turakulovd0e2cd22017-11-15 18:22:062892 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 17:45:592893
Andrii Shyshkalov2d8a2072017-04-10 13:02:182894 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 17:43:582895 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 13:02:182896
Aaron Gable9b713dd2016-12-15 00:04:212897 if title:
Nick Carter8692b182017-11-07 00:30:382898 # Punctuation and whitespace in |title| must be percent-encoded.
2899 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
[email protected]bf766ba2016-04-13 12:51:232900
agablec6787972016-09-09 23:13:342901 if options.private:
Aaron Gableb02daf02017-05-23 18:53:462902 refspec_opts.append('private')
agablec6787972016-09-09 23:13:342903
Andrii Shyshkalov2f727912018-10-15 17:02:332904 for r in sorted(reviewers):
2905 if r in valid_accounts:
2906 refspec_opts.append('r=%s' % r)
2907 reviewers.remove(r)
2908 else:
2909 # TODO(tandrii): this should probably be a hard failure.
2910 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2911 % r)
2912 for c in sorted(cc):
2913 # refspec option will be rejected if cc doesn't correspond to an
2914 # account, even though REST call to add such arbitrary cc may succeed.
2915 if c in valid_accounts:
2916 refspec_opts.append('cc=%s' % c)
2917 cc.remove(c)
2918
2919
rmistry9eadede2016-09-19 18:22:432920 if options.topic:
2921 # Documentation on Gerrit topics is here:
2922 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:202923 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 18:22:432924
Nodir Turakulovd0e2cd22017-11-15 18:22:062925 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 19:04:252926 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 18:22:062927 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 19:04:252928 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 18:22:062929 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2930
Andrii Shyshkalovfebbae92017-04-05 15:05:202931 refspec_suffix = ''
2932 if refspec_opts:
2933 refspec_suffix = '%' + ','.join(refspec_opts)
2934 assert ' ' not in refspec_suffix, (
2935 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2936 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2937
Andrii Shyshkalov7d518832016-12-15 19:48:212938 try:
Edward Lemur83bd7f42018-10-10 00:14:212939 # TODO(crbug.com/881860): Remove.
Edward Lemur47faa062018-10-11 19:46:022940 # Clear the log after each git-cl upload run by setting mode='w'.
2941 handler = logging.FileHandler(gerrit_util.GERRIT_ERR_LOG_FILE, mode='w')
2942 handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
2943
2944 GERRIT_ERR_LOGGER.addHandler(handler)
2945 GERRIT_ERR_LOGGER.setLevel(logging.INFO)
2946 # Don't propagate to root logger, so that logs are not printed.
2947 GERRIT_ERR_LOGGER.propagate = 0
2948
Edward Lemur83bd7f42018-10-10 00:14:212949 # Get interesting headers from git push, to be displayed to the user if
2950 # subsequent Gerrit RPC calls fail.
2951 env = os.environ.copy()
2952 env['GIT_CURL_VERBOSE'] = '1'
2953 class FilterHeaders(object):
2954 """Filter git push headers and store them in a file.
2955
2956 Regular git push output is printed directly.
2957 """
2958
2959 def __init__(self):
2960 # The output from git push that we want to store in a file.
2961 self._output = ''
2962 # Keeps track of whether the current line is part of a request header.
2963 self._on_header = False
2964 # Keeps track of repeated empty lines, which mark the end of a request
2965 # header.
2966 self._last_line_empty = False
2967
2968 def __call__(self, line):
2969 """Handle a single line of git push output."""
2970 if not line:
2971 # Two consecutive empty lines mark the end of a header.
2972 if self._last_line_empty:
2973 self._on_header = False
2974 self._last_line_empty = True
2975 return
2976
2977 self._last_line_empty = False
2978 # A line starting with '>' marks the beggining of a request header.
2979 if line[0] == '>':
2980 self._on_header = True
2981 GERRIT_ERR_LOGGER.info(line)
2982 # Lines not starting with '*' or '<', and not part of a request header
2983 # should be displayed to the user.
2984 elif line[0] not in '*<' and not self._on_header:
2985 print(line)
2986 # Flush after every line: useful for seeing progress when running as
2987 # recipe.
2988 sys.stdout.flush()
2989 # Filter out the cookie and authorization headers.
2990 elif ('cookie: ' not in line.lower()
2991 and 'authorization: ' not in line.lower()):
2992 GERRIT_ERR_LOGGER.info(line)
2993
2994 filter_fn = FilterHeaders()
Andrii Shyshkalov7d518832016-12-15 19:48:212995 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:412996 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemur83bd7f42018-10-10 00:14:212997 print_stdout=False,
2998 filter_fn=filter_fn,
2999 env=env)
Andrii Shyshkalov7d518832016-12-15 19:48:213000 except subprocess2.CalledProcessError:
3001 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 12:28:233002 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 20:39:423003 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 12:28:233004 'credential problems:\n'
3005 ' git cl creds-check\n',
3006 change_desc)
[email protected]aa6235b2016-04-11 21:35:293007
3008 if options.squash:
Aaron Gable289b4312017-09-13 21:06:163009 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
[email protected]aa6235b2016-04-11 21:35:293010 change_numbers = [m.group(1)
3011 for m in map(regex.match, push_stdout.splitlines())
3012 if m]
3013 if len(change_numbers) != 1:
3014 DieWithError(
3015 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 01:40:113016 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
[email protected]aa6235b2016-04-11 21:35:293017 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 23:19:373018 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 11:29:573019
Andrii Shyshkalov2f727912018-10-15 17:02:333020 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:583021 # GetIssue() is not set in case of non-squash uploads according to tests.
3022 # TODO(agable): non-squash uploads in git cl should be removed.
3023 gerrit_util.AddReviewers(
3024 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:193025 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:583026 reviewers, cc,
3027 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 21:27:583028
Aaron Gablefd238082017-06-07 20:42:343029 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 06:33:343030 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3031 score = 1
3032 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3033 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3034 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 20:42:343035 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:583036 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:193037 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 06:33:343038 msg='Self-approving for TBR',
3039 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 20:42:343040
Andrii Shyshkalovdd788442018-10-13 17:55:293041 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
3042 options.cq_dry_run)
[email protected]aa6235b2016-04-11 21:35:293043 return 0
3044
Andrii Shyshkalov550e9242017-04-12 15:14:493045 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3046 change_desc):
3047 """Computes parent of the generated commit to be uploaded to Gerrit.
3048
3049 Returns revision or a ref name.
3050 """
3051 if custom_cl_base:
3052 # Try to avoid creating additional unintended CLs when uploading, unless
3053 # user wants to take this risk.
3054 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3055 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3056 local_ref_of_target_remote])
3057 if code == 1:
Quinten Yearsley0c62da92017-05-31 20:39:423058 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 15:14:493059 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3060 'If you proceed with upload, more than 1 CL may be created by '
3061 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3062 'If you are certain that specified base `%s` has already been '
3063 'uploaded to Gerrit as another CL, you may proceed.\n' %
3064 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3065 if not force:
3066 confirm_or_exit(
3067 'Do you take responsibility for cleaning up potential mess '
3068 'resulting from proceeding with upload?',
3069 action='upload')
3070 return custom_cl_base
3071
Aaron Gablef97e33d2017-03-30 22:44:273072 if remote != '.':
3073 return self.GetCommonAncestorWithUpstream()
3074
3075 # If our upstream branch is local, we base our squashed commit on its
3076 # squashed version.
3077 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3078
Aaron Gablef97e33d2017-03-30 22:44:273079 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 21:37:083080 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 22:44:273081
3082 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 15:14:493083 # TODO(tandrii): consider checking parent change in Gerrit and using its
3084 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3085 # the tree hash of the parent branch. The upside is less likely bogus
3086 # requests to reupload parent change just because it's uploadhash is
3087 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 22:44:273088 parent = RunGit(['config',
3089 'branch.%s.gerritsquashhash' % upstream_branch_name],
3090 error_ok=True).strip()
3091 # Verify that the upstream branch has been uploaded too, otherwise
3092 # Gerrit will create additional CLs when uploading.
3093 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3094 RunGitSilent(['rev-parse', parent + ':'])):
3095 DieWithError(
3096 '\nUpload upstream branch %s first.\n'
3097 'It is likely that this branch has been rebased since its last '
3098 'upload, so you just need to upload it again.\n'
3099 '(If you uploaded it with --no-squash, then branch dependencies '
3100 'are not supported, and you should reupload with --squash.)'
3101 % upstream_branch_name,
3102 change_desc)
3103 return parent
3104
[email protected]8930b3d2016-04-13 14:47:023105 def _AddChangeIdToCommitMessage(self, options, args):
3106 """Re-commits using the current message, assumes the commit hook is in
3107 place.
3108 """
Andrii Shyshkalovb07575f2018-10-16 06:16:213109 log_desc = options.message or _create_description_from_log(args)
[email protected]8930b3d2016-04-13 14:47:023110 git_command = ['commit', '--amend', '-m', log_desc]
3111 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:213112 new_log_desc = _create_description_from_log(args)
[email protected]8930b3d2016-04-13 14:47:023113 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 16:17:493114 print('git-cl: Added Change-Id to commit message.')
[email protected]8930b3d2016-04-13 14:47:023115 return new_log_desc
3116 else:
[email protected]b067ec52016-05-31 15:24:443117 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
[email protected]aa6235b2016-04-11 21:35:293118
Ravi Mistry31e7d562018-04-02 16:53:573119 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3120 """Sets labels on the change based on the provided flags."""
3121 labels = {}
3122 notify = None;
3123 if enable_auto_submit:
3124 labels['Auto-Submit'] = 1
3125 if use_commit_queue:
3126 labels['Commit-Queue'] = 2
3127 elif cq_dry_run:
3128 labels['Commit-Queue'] = 1
3129 notify = False
3130 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:193131 gerrit_util.SetReview(
3132 self._GetGerritHost(),
3133 self._GerritChangeIdentifier(),
3134 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 16:53:573135
[email protected]fa330e82016-04-13 17:09:523136 def SetCQState(self, new_state):
3137 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
[email protected]fa330e82016-04-13 17:09:523138 vote_map = {
3139 _CQState.NONE: 0,
3140 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 15:44:133141 _CQState.COMMIT: 2,
[email protected]fa330e82016-04-13 17:09:523142 }
Aaron Gablefc62f762017-07-17 18:12:073143 labels = {'Commit-Queue': vote_map[new_state]}
3144 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:063145 gerrit_util.SetReview(
3146 self._GetGerritHost(), self._GerritChangeIdentifier(),
3147 labels=labels, notify=notify)
[email protected]fa330e82016-04-13 17:09:523148
tandriie113dfd2016-10-11 17:20:123149 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 14:52:023150 try:
3151 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 23:14:383152 except GerritChangeNotExists:
3153 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 14:52:023154
3155 if data['status'] in ('ABANDONED', 'MERGED'):
3156 return 'CL %s is closed' % self.GetIssue()
3157
Quinten Yearsley0c62da92017-05-31 20:39:423158 def GetTryJobProperties(self, patchset=None):
3159 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 14:52:023160 data = self._GetChangeDetail(['ALL_REVISIONS'])
3161 patchset = int(patchset or self.GetPatchset())
3162 assert patchset
3163 revision_data = None # Pylint wants it to be defined.
3164 for revision_data in data['revisions'].itervalues():
3165 if int(revision_data['_number']) == patchset:
3166 break
3167 else:
Aaron Gablea45ee112016-11-22 23:14:383168 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 14:52:023169 (patchset, self.GetIssue()))
3170 return {
3171 'patch_issue': self.GetIssue(),
3172 'patch_set': patchset or self.GetPatchset(),
3173 'patch_project': data['project'],
3174 'patch_storage': 'gerrit',
3175 'patch_ref': revision_data['fetch']['http']['ref'],
3176 'patch_repository_url': revision_data['fetch']['http']['url'],
3177 'patch_gerrit_url': self.GetCodereviewServer(),
3178 }
tandriie113dfd2016-10-11 17:20:123179
tandriide281ae2016-10-12 13:02:303180 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 14:52:023181 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 13:02:303182
Edward Lemur707d70b2018-02-06 23:50:143183 def GetReviewers(self):
3184 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3185 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3186
[email protected]d68b62b2016-03-31 16:09:293187
3188_CODEREVIEW_IMPLEMENTATIONS = {
3189 'rietveld': _RietveldChangelistImpl,
3190 'gerrit': _GerritChangelistImpl,
3191}
3192
[email protected]013a2802016-03-29 09:52:333193
iannuccie53c9352016-08-17 21:40:403194def _add_codereview_issue_select_options(parser, extra=""):
3195 _add_codereview_select_options(parser)
3196
3197 text = ('Operate on this issue number instead of the current branch\'s '
3198 'implicit issue.')
3199 if extra:
3200 text += ' '+extra
3201 parser.add_option('-i', '--issue', type=int, help=text)
3202
3203
3204def _process_codereview_issue_select_options(parser, options):
3205 _process_codereview_select_options(parser, options)
3206 if options.issue is not None and not options.forced_codereview:
3207 parser.error('--issue must be specified with either --rietveld or --gerrit')
3208
3209
[email protected]dde64622016-04-13 17:11:213210def _add_codereview_select_options(parser):
3211 """Appends --gerrit and --rietveld options to force specific codereview."""
3212 parser.codereview_group = optparse.OptionGroup(
3213 parser, 'EXPERIMENTAL! Codereview override options')
3214 parser.add_option_group(parser.codereview_group)
3215 parser.codereview_group.add_option(
3216 '--gerrit', action='store_true',
3217 help='Force the use of Gerrit for codereview')
3218 parser.codereview_group.add_option(
3219 '--rietveld', action='store_true',
3220 help='Force the use of Rietveld for codereview')
3221
3222
3223def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:473224 if options.rietveld:
3225 parser.error('--rietveld is no longer supported')
[email protected]dde64622016-04-13 17:11:213226 options.forced_codereview = None
3227 if options.gerrit:
3228 options.forced_codereview = 'gerrit'
[email protected]dde64622016-04-13 17:11:213229
3230
tandriif9aefb72016-07-01 16:06:513231def _get_bug_line_values(default_project, bugs):
3232 """Given default_project and comma separated list of bugs, yields bug line
3233 values.
3234
3235 Each bug can be either:
3236 * a number, which is combined with default_project
3237 * string, which is left as is.
3238
3239 This function may produce more than one line, because bugdroid expects one
3240 project per line.
3241
3242 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3243 ['v8:123', 'chromium:789']
3244 """
3245 default_bugs = []
3246 others = []
3247 for bug in bugs.split(','):
3248 bug = bug.strip()
3249 if bug:
3250 try:
3251 default_bugs.append(int(bug))
3252 except ValueError:
3253 others.append(bug)
3254
3255 if default_bugs:
3256 default_bugs = ','.join(map(str, default_bugs))
3257 if default_project:
3258 yield '%s:%s' % (default_project, default_bugs)
3259 else:
3260 yield default_bugs
3261 for other in sorted(others):
3262 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3263 yield other
3264
3265
[email protected]20254fc2011-03-22 18:28:593266class ChangeDescription(object):
3267 """Contains a parsed form of the change description."""
[email protected]c6f60e82013-04-19 17:01:573268 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 19:20:233269 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 17:51:553270 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 13:34:083271 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 19:04:253272 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3273 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3274 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3275 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
[email protected]20254fc2011-03-22 18:28:593276
[email protected]78936cb2013-04-11 00:17:523277 def __init__(self, description):
[email protected]42c20792013-09-12 17:34:493278 self._description_lines = (description or '').strip().splitlines()
[email protected]78936cb2013-04-11 00:17:523279
[email protected]42c20792013-09-12 17:34:493280 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 21:53:263281 def description(self): # pylint: disable=method-hidden
[email protected]42c20792013-09-12 17:34:493282 return '\n'.join(self._description_lines)
3283
3284 def set_description(self, desc):
3285 if isinstance(desc, basestring):
3286 lines = desc.splitlines()
3287 else:
3288 lines = [line.rstrip() for line in desc]
3289 while lines and not lines[0]:
3290 lines.pop(0)
3291 while lines and not lines[-1]:
3292 lines.pop(-1)
3293 self._description_lines = lines
[email protected]78936cb2013-04-11 00:17:523294
Robert Iannucci6c98dc62017-04-18 18:38:003295 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3296 """Rewrites the R=/TBR= line(s) as a single line each.
3297
3298 Args:
3299 reviewers (list(str)) - list of additional emails to use for reviewers.
3300 tbrs (list(str)) - list of additional emails to use for TBRs.
3301 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3302 the change that are missing OWNER coverage. If this is not None, you
3303 must also pass a value for `change`.
3304 change (Change) - The Change that should be used for OWNERS lookups.
3305 """
[email protected]78936cb2013-04-11 00:17:523306 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 18:38:003307 assert isinstance(tbrs, list), tbrs
3308
Robert Iannuccif2708bd2017-04-17 22:49:023309 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 20:14:493310 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 18:38:003311
3312 if not reviewers and not tbrs and not add_owners_to:
[email protected]78936cb2013-04-11 00:17:523313 return
Robert Iannucci6c98dc62017-04-18 18:38:003314
3315 reviewers = set(reviewers)
3316 tbrs = set(tbrs)
3317 LOOKUP = {
3318 'TBR': tbrs,
3319 'R': reviewers,
3320 }
[email protected]78936cb2013-04-11 00:17:523321
Quinten Yearsley0c62da92017-05-31 20:39:423322 # Get the set of R= and TBR= lines and remove them from the description.
[email protected]42c20792013-09-12 17:34:493323 regexp = re.compile(self.R_LINE)
3324 matches = [regexp.match(line) for line in self._description_lines]
3325 new_desc = [l for i, l in enumerate(self._description_lines)
3326 if not matches[i]]
3327 self.set_description(new_desc)
[email protected]78936cb2013-04-11 00:17:523328
[email protected]42c20792013-09-12 17:34:493329 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 18:38:003330
3331 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
[email protected]42c20792013-09-12 17:34:493332 for match in matches:
3333 if not match:
3334 continue
Robert Iannucci6c98dc62017-04-18 18:38:003335 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3336
3337 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 22:49:023338 if add_owners_to:
[email protected]336f9122014-09-04 02:16:553339 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 08:44:183340 fopen=file, os_path=os.path)
[email protected]336f9122014-09-04 02:16:553341 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-19 00:28:263342 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 18:38:003343 LOOKUP[add_owners_to].update(
3344 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 22:49:023345
Robert Iannucci6c98dc62017-04-18 18:38:003346 # If any folks ended up in both groups, remove them from tbrs.
3347 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 22:49:023348
Robert Iannucci6c98dc62017-04-18 18:38:003349 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3350 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
[email protected]42c20792013-09-12 17:34:493351
3352 # Put the new lines in the description where the old first R= line was.
3353 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3354 if 0 <= line_loc < len(self._description_lines):
3355 if new_tbr_line:
3356 self._description_lines.insert(line_loc, new_tbr_line)
3357 if new_r_line:
3358 self._description_lines.insert(line_loc, new_r_line)
[email protected]78936cb2013-04-11 00:17:523359 else:
[email protected]42c20792013-09-12 17:34:493360 if new_r_line:
3361 self.append_footer(new_r_line)
3362 if new_tbr_line:
3363 self.append_footer(new_tbr_line)
[email protected]78936cb2013-04-11 00:17:523364
Aaron Gable3a16ed12017-03-23 17:51:553365 def prompt(self, bug=None, git_footer=True):
[email protected]78936cb2013-04-11 00:17:523366 """Asks the user to update the description."""
[email protected]42c20792013-09-12 17:34:493367 self.set_description([
3368 '# Enter a description of the change.',
3369 '# This will be displayed on the codereview site.',
3370 '# The first line will also be used as the subject of the review.',
[email protected]bd1073e2013-06-01 00:34:383371 '#--------------------This line is 72 characters long'
[email protected]42c20792013-09-12 17:34:493372 '--------------------',
3373 ] + self._description_lines)
[email protected]78936cb2013-04-11 00:17:523374
[email protected]42c20792013-09-12 17:34:493375 regexp = re.compile(self.BUG_LINE)
3376 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 16:06:513377 prefix = settings.GetBugPrefix()
3378 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 17:51:553379 if git_footer:
3380 self.append_footer('Bug: %s' % ', '.join(values))
3381 else:
3382 for value in values:
3383 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 16:06:513384
[email protected]42c20792013-09-12 17:34:493385 content = gclient_utils.RunEditor(self.description, True,
[email protected]615a2622013-05-03 13:20:143386 git_editor=settings.GetGitEditor())
[email protected]0e0436a2011-10-25 13:32:413387 if not content:
3388 DieWithError('Running editor failed')
[email protected]42c20792013-09-12 17:34:493389 lines = content.splitlines()
[email protected]78936cb2013-04-11 00:17:523390
Bruce Dawson2377b012018-01-12 00:46:493391 # Strip off comments and default inserted "Bug:" line.
3392 clean_lines = [line.rstrip() for line in lines if not
3393 (line.startswith('#') or line.rstrip() == "Bug:")]
[email protected]42c20792013-09-12 17:34:493394 if not clean_lines:
[email protected]0e0436a2011-10-25 13:32:413395 DieWithError('No CL description, aborting')
[email protected]42c20792013-09-12 17:34:493396 self.set_description(clean_lines)
[email protected]20254fc2011-03-22 18:28:593397
[email protected]78936cb2013-04-11 00:17:523398 def append_footer(self, line):
[email protected]601e1d12016-06-03 13:03:543399 """Adds a footer line to the description.
3400
3401 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3402 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3403 that Gerrit footers are always at the end.
3404 """
3405 parsed_footer_line = git_footers.parse_footer(line)
3406 if parsed_footer_line:
3407 # Line is a gerrit footer in the form: Footer-Key: any value.
3408 # Thus, must be appended observing Gerrit footer rules.
3409 self.set_description(
3410 git_footers.add_footer(self.description,
3411 key=parsed_footer_line[0],
3412 value=parsed_footer_line[1]))
3413 return
3414
3415 if not self._description_lines:
3416 self._description_lines.append(line)
3417 return
3418
3419 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3420 if gerrit_footers:
3421 # git_footers.split_footers ensures that there is an empty line before
3422 # actual (gerrit) footers, if any. We have to keep it that way.
3423 assert top_lines and top_lines[-1] == ''
3424 top_lines, separator = top_lines[:-1], top_lines[-1:]
3425 else:
3426 separator = [] # No need for separator if there are no gerrit_footers.
3427
3428 prev_line = top_lines[-1] if top_lines else ''
3429 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3430 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3431 top_lines.append('')
3432 top_lines.append(line)
3433 self._description_lines = top_lines + separator + gerrit_footers
[email protected]20254fc2011-03-22 18:28:593434
tandrii99a72f22016-08-17 21:33:243435 def get_reviewers(self, tbr_only=False):
[email protected]78936cb2013-04-11 00:17:523436 """Retrieves the list of reviewers."""
[email protected]42c20792013-09-12 17:34:493437 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 21:33:243438 reviewers = [match.group(2).strip()
3439 for match in matches
3440 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
[email protected]78936cb2013-04-11 00:17:523441 return cleanup_list(reviewers)
[email protected]20254fc2011-03-22 18:28:593442
bradnelsond975b302016-10-23 19:20:233443 def get_cced(self):
3444 """Retrieves the list of reviewers."""
3445 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3446 cced = [match.group(2).strip() for match in matches if match]
3447 return cleanup_list(cced)
3448
Nodir Turakulov23b82142017-11-16 19:04:253449 def get_hash_tags(self):
3450 """Extracts and sanitizes a list of Gerrit hashtags."""
3451 subject = (self._description_lines or ('',))[0]
3452 subject = re.sub(
3453 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3454
3455 tags = []
3456 start = 0
3457 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3458 while True:
3459 m = bracket_exp.match(subject, start)
3460 if not m:
3461 break
3462 tags.append(self.sanitize_hash_tag(m.group(1)))
3463 start = m.end()
3464
3465 if not tags:
3466 # Try "Tag: " prefix.
3467 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3468 if m:
3469 tags.append(self.sanitize_hash_tag(m.group(1)))
3470 return tags
3471
3472 @classmethod
3473 def sanitize_hash_tag(cls, tag):
3474 """Returns a sanitized Gerrit hash tag.
3475
3476 A sanitized hashtag can be used as a git push refspec parameter value.
3477 """
3478 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3479
Andrii Shyshkalov15e50cc2016-12-02 13:34:083480 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3481 """Updates this commit description given the parent.
3482
3483 This is essentially what Gnumbd used to do.
3484 Consult https://goo.gl/WMmpDe for more details.
3485 """
3486 assert parent_msg # No, orphan branch creation isn't supported.
3487 assert parent_hash
3488 assert dest_ref
3489 parent_footer_map = git_footers.parse_footers(parent_msg)
3490 # This will also happily parse svn-position, which GnumbD is no longer
3491 # supporting. While we'd generate correct footers, the verifier plugin
3492 # installed in Gerrit will block such commit (ie git push below will fail).
3493 parent_position = git_footers.get_position(parent_footer_map)
3494
3495 # Cherry-picks may have last line obscuring their prior footers,
3496 # from git_footers perspective. This is also what Gnumbd did.
3497 cp_line = None
3498 if (self._description_lines and
3499 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3500 cp_line = self._description_lines.pop()
3501
Andrii Shyshkalovde37c012017-07-06 19:06:503502 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 13:34:083503
3504 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3505 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 19:06:503506 for i, line in enumerate(footer_lines):
3507 k, v = git_footers.parse_footer(line) or (None, None)
3508 if k and k.startswith('Cr-'):
3509 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 13:34:083510
3511 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 18:35:123512 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 13:34:083513 if parent_position[0] == dest_ref:
3514 # Same branch as parent.
3515 number = int(parent_position[1]) + 1
3516 else:
3517 number = 1 # New branch, and extra lineage.
3518 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3519 int(parent_position[1])))
3520
Andrii Shyshkalovde37c012017-07-06 19:06:503521 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3522 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 13:34:083523
3524 self._description_lines = top_lines
3525 if cp_line:
3526 self._description_lines.append(cp_line)
3527 if self._description_lines[-1] != '':
3528 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 19:06:503529 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 13:34:083530
[email protected]20254fc2011-03-22 18:28:593531
Aaron Gablea1bab272017-04-11 23:38:183532def get_approving_reviewers(props, disapproval=False):
[email protected]e52678e2013-04-26 18:34:443533 """Retrieves the reviewers that approved a CL from the issue properties with
3534 messages.
3535
3536 Note that the list may contain reviewers that are not committer, thus are not
3537 considered by the CQ.
Aaron Gablea1bab272017-04-11 23:38:183538
3539 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
[email protected]e52678e2013-04-26 18:34:443540 """
Aaron Gablea1bab272017-04-11 23:38:183541 approval_type = 'disapproval' if disapproval else 'approval'
[email protected]e52678e2013-04-26 18:34:443542 return sorted(
3543 set(
3544 message['sender']
3545 for message in props['messages']
Aaron Gablea1bab272017-04-11 23:38:183546 if message[approval_type] and message['sender'] in props['reviewers']
[email protected]e52678e2013-04-26 18:34:443547 )
3548 )
3549
3550
[email protected]cc51cd02010-12-23 00:48:393551def FindCodereviewSettingsFile(filename='codereview.settings'):
3552 """Finds the given file starting in the cwd and going up.
3553
3554 Only looks up to the top of the repository unless an
3555 'inherit-review-settings-ok' file exists in the root of the repository.
3556 """
3557 inherit_ok_file = 'inherit-review-settings-ok'
3558 cwd = os.getcwd()
[email protected]8b0553c2014-02-11 00:33:373559 root = settings.GetRoot()
[email protected]cc51cd02010-12-23 00:48:393560 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3561 root = '/'
3562 while True:
3563 if filename in os.listdir(cwd):
3564 if os.path.isfile(os.path.join(cwd, filename)):
3565 return open(os.path.join(cwd, filename))
3566 if cwd == root:
3567 break
3568 cwd = os.path.dirname(cwd)
3569
3570
3571def LoadCodereviewSettingsFromFile(fileobj):
3572 """Parse a codereview.settings file and updates hooks."""
[email protected]99ac1c52012-01-16 14:52:123573 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
[email protected]cc51cd02010-12-23 00:48:393574
[email protected]cc51cd02010-12-23 00:48:393575 def SetProperty(name, setting, unset_error_ok=False):
3576 fullname = 'rietveld.' + name
3577 if setting in keyvals:
3578 RunGit(['config', fullname, keyvals[setting]])
3579 else:
3580 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3581
tandrii48df5812016-10-17 10:55:373582 if not keyvals.get('GERRIT_HOST', False):
3583 SetProperty('server', 'CODE_REVIEW_SERVER')
[email protected]cc51cd02010-12-23 00:48:393584 # Only server setting is required. Other settings can be absent.
3585 # In that case, we ignore errors raised during option deletion attempt.
3586 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
[email protected]c1737d02013-05-29 14:17:283587 SetProperty('private', 'PRIVATE', unset_error_ok=True)
[email protected]cc51cd02010-12-23 00:48:393588 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3589 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
[email protected]90752582014-01-14 21:04:503590 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
[email protected]44202a22014-03-11 19:22:183591 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3592 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
[email protected]152cf832014-06-11 21:37:493593 SetProperty('project', 'PROJECT', unset_error_ok=True)
[email protected]5626a922015-02-26 14:03:303594 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3595 unset_error_ok=True)
[email protected]cc51cd02010-12-23 00:48:393596
[email protected]7044efc2013-11-28 01:51:213597 if 'GERRIT_HOST' in keyvals:
[email protected]e8077812012-02-03 03:41:463598 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
[email protected]e8077812012-02-03 03:41:463599
[email protected]54b400c2016-01-14 10:08:253600 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 20:24:233601 RunGit(['config', 'gerrit.squash-uploads',
3602 keyvals['GERRIT_SQUASH_UPLOADS']])
[email protected]54b400c2016-01-14 10:08:253603
[email protected]28253532016-04-14 13:46:563604 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
[email protected]00dbccd2016-04-15 07:24:433605 RunGit(['config', 'gerrit.skip-ensure-authenticated',
[email protected]28253532016-04-14 13:46:563606 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3607
[email protected]cc51cd02010-12-23 00:48:393608 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 15:44:133609 # should be of the form
3610 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3611 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
[email protected]cc51cd02010-12-23 00:48:393612 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3613 keyvals['ORIGIN_URL_CONFIG']])
3614
[email protected]cc51cd02010-12-23 00:48:393615
[email protected]426f69b2012-08-02 23:41:493616def urlretrieve(source, destination):
3617 """urllib is broken for SSL connections via a proxy therefore we
3618 can't use urllib.urlretrieve()."""
3619 with open(destination, 'w') as f:
3620 f.write(urllib2.urlopen(source).read())
3621
3622
[email protected]712d6102013-11-27 00:52:583623def hasSheBang(fname):
3624 """Checks fname is a #! script."""
3625 with open(fname) as f:
3626 return f.read(2).startswith('#!')
3627
3628
[email protected]917f0ff2016-04-05 00:45:303629# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3630def DownloadHooks(*args, **kwargs):
3631 pass
3632
3633
[email protected]18630d62016-03-04 12:06:023634def DownloadGerritHook(force):
3635 """Download and install Gerrit commit-msg hook.
[email protected]78c4b982012-02-14 02:20:263636
3637 Args:
3638 force: True to update hooks. False to install hooks if not present.
3639 """
3640 if not settings.GetIsGerrit():
3641 return
[email protected]712d6102013-11-27 00:52:583642 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
[email protected]78c4b982012-02-14 02:20:263643 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3644 if not os.access(dst, os.X_OK):
3645 if os.path.exists(dst):
3646 if not force:
3647 return
[email protected]78c4b982012-02-14 02:20:263648 try:
[email protected]426f69b2012-08-02 23:41:493649 urlretrieve(src, dst)
[email protected]712d6102013-11-27 00:52:583650 if not hasSheBang(dst):
3651 DieWithError('Not a script: %s\n'
3652 'You need to download from\n%s\n'
3653 'into .git/hooks/commit-msg and '
3654 'chmod +x .git/hooks/commit-msg' % (dst, src))
[email protected]78c4b982012-02-14 02:20:263655 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3656 except Exception:
3657 if os.path.exists(dst):
3658 os.remove(dst)
[email protected]712d6102013-11-27 00:52:583659 DieWithError('\nFailed to download hooks.\n'
3660 'You need to download from\n%s\n'
3661 'into .git/hooks/commit-msg and '
3662 'chmod +x .git/hooks/commit-msg' % src)
[email protected]78c4b982012-02-14 02:20:263663
3664
[email protected]e7d3d162016-03-15 14:15:573665def GetRietveldCodereviewSettingsInteractively():
3666 """Prompt the user for settings."""
3667 server = settings.GetDefaultServerUrl(error_ok=True)
3668 prompt = 'Rietveld server (host[:port])'
3669 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3670 newserver = ask_for_data(prompt + ':')
3671 if not server and not newserver:
3672 newserver = DEFAULT_SERVER
3673 if newserver:
3674 newserver = gclient_utils.UpgradeToHttps(newserver)
3675 if newserver != server:
3676 RunGit(['config', 'rietveld.server', newserver])
3677
3678 def SetProperty(initial, caption, name, is_url):
3679 prompt = caption
3680 if initial:
3681 prompt += ' ("x" to clear) [%s]' % initial
3682 new_val = ask_for_data(prompt + ':')
3683 if new_val == 'x':
3684 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3685 elif new_val:
3686 if is_url:
3687 new_val = gclient_utils.UpgradeToHttps(new_val)
3688 if new_val != initial:
3689 RunGit(['config', 'rietveld.' + name, new_val])
3690
3691 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3692 SetProperty(settings.GetDefaultPrivateFlag(),
3693 'Private flag (rietveld only)', 'private', False)
3694 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3695 'tree-status-url', False)
3696 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3697 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3698 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3699 'run-post-upload-hook', False)
3700
Andrii Shyshkalov18975322017-01-25 15:44:133701
Andrii Shyshkalov517948b2017-03-15 14:51:593702class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 20:39:423703 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 15:52:183704
Andrii Shyshkalov34924cd2017-03-15 16:08:323705 _GOOGLESOURCE = 'googlesource.com'
3706
3707 def __init__(self):
3708 # Cached list of [host, identity, source], where source is either
3709 # .gitcookies or .netrc.
3710 self._all_hosts = None
3711
Andrii Shyshkalov517948b2017-03-15 14:51:593712 def ensure_configured_gitcookies(self):
3713 """Runs checks and suggests fixes to make git use .gitcookies from default
3714 path."""
3715 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3716 configured_path = RunGitSilent(
3717 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 13:39:313718 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 14:51:593719 if configured_path:
3720 self._ensure_default_gitcookies_path(configured_path, default)
3721 else:
3722 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 15:52:183723
Andrii Shyshkalov517948b2017-03-15 14:51:593724 @staticmethod
3725 def _ensure_default_gitcookies_path(configured_path, default_path):
3726 assert configured_path
3727 if configured_path == default_path:
3728 print('git is already configured to use your .gitcookies from %s' %
3729 configured_path)
3730 return
3731
Quinten Yearsley0c62da92017-05-31 20:39:423732 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 14:51:593733 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3734 (configured_path, default_path))
3735
3736 if not os.path.exists(configured_path):
3737 print('However, your configured .gitcookies file is missing.')
3738 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3739 action='reconfigure')
3740 RunGit(['config', '--global', 'http.cookiefile', default_path])
3741 return
3742
3743 if os.path.exists(default_path):
3744 print('WARNING: default .gitcookies file already exists %s' %
3745 default_path)
3746 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3747 default_path)
3748
3749 confirm_or_exit('Move existing .gitcookies to default location?',
3750 action='move')
3751 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 15:52:183752 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 14:51:593753 print('Moved and reconfigured git to use .gitcookies from %s' %
3754 default_path)
Andrii Shyshkalov353637c2017-03-14 15:52:183755
Andrii Shyshkalov517948b2017-03-15 14:51:593756 @staticmethod
3757 def _configure_gitcookies_path(default_path):
3758 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3759 if os.path.exists(netrc_path):
3760 print('You seem to be using outdated .netrc for git credentials: %s' %
3761 netrc_path)
3762 print('This tool will guide you through setting up recommended '
3763 '.gitcookies store for git credentials.\n'
3764 '\n'
3765 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3766 ' git config --global --unset http.cookiefile\n'
3767 ' mv %s %s.backup\n\n' % (default_path, default_path))
3768 confirm_or_exit(action='setup .gitcookies')
3769 RunGit(['config', '--global', 'http.cookiefile', default_path])
3770 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 15:52:183771
Andrii Shyshkalov34924cd2017-03-15 16:08:323772 def get_hosts_with_creds(self, include_netrc=False):
3773 if self._all_hosts is None:
3774 a = gerrit_util.CookiesAuthenticator()
3775 self._all_hosts = [
3776 (h, u, s)
3777 for h, u, s in itertools.chain(
3778 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3779 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3780 )
3781 if h.endswith(self._GOOGLESOURCE)
3782 ]
3783
3784 if include_netrc:
3785 return self._all_hosts
3786 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3787
3788 def print_current_creds(self, include_netrc=False):
3789 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3790 if not hosts:
3791 print('No Git/Gerrit credentials found')
3792 return
3793 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3794 header = [('Host', 'User', 'Which file'),
3795 ['=' * l for l in lengths]]
3796 for row in (header + hosts):
3797 print('\t'.join((('%%+%ds' % l) % s)
3798 for l, s in zip(lengths, row)))
3799
Andrii Shyshkalov97800502017-03-16 15:04:323800 @staticmethod
3801 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 23:16:143802 """Parses identity "git-<username>.domain" into <username> and domain."""
3803 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 13:17:553804 # distinguishable from sub-domains. But we do know typical domains:
3805 if identity.endswith('.chromium.org'):
3806 domain = 'chromium.org'
3807 username = identity[:-len('.chromium.org')]
3808 else:
3809 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 15:04:323810 if username.startswith('git-'):
3811 username = username[len('git-'):]
3812 return username, domain
3813
3814 def _get_usernames_of_domain(self, domain):
3815 """Returns list of usernames referenced by .gitcookies in a given domain."""
3816 identities_by_domain = {}
3817 for _, identity, _ in self.get_hosts_with_creds():
3818 username, domain = self._parse_identity(identity)
3819 identities_by_domain.setdefault(domain, []).append(username)
3820 return identities_by_domain.get(domain)
3821
3822 def _canonical_git_googlesource_host(self, host):
3823 """Normalizes Gerrit hosts (with '-review') to Git host."""
3824 assert host.endswith(self._GOOGLESOURCE)
3825 # Prefix doesn't include '.' at the end.
3826 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3827 if prefix.endswith('-review'):
3828 prefix = prefix[:-len('-review')]
3829 return prefix + '.' + self._GOOGLESOURCE
3830
Andrii Shyshkalov0a0b0672017-03-16 15:27:483831 def _canonical_gerrit_googlesource_host(self, host):
3832 git_host = self._canonical_git_googlesource_host(host)
3833 prefix = git_host.split('.', 1)[0]
3834 return prefix + '-review.' + self._GOOGLESOURCE
3835
Andrii Shyshkalovc8173822017-07-10 10:10:533836 def _get_counterpart_host(self, host):
3837 assert host.endswith(self._GOOGLESOURCE)
3838 git = self._canonical_git_googlesource_host(host)
3839 gerrit = self._canonical_gerrit_googlesource_host(git)
3840 return git if gerrit == host else gerrit
3841
Andrii Shyshkalov97800502017-03-16 15:04:323842 def has_generic_host(self):
3843 """Returns whether generic .googlesource.com has been configured.
3844
3845 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3846 """
3847 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3848 if host == '.' + self._GOOGLESOURCE:
3849 return True
3850 return False
3851
3852 def _get_git_gerrit_identity_pairs(self):
3853 """Returns map from canonic host to pair of identities (Git, Gerrit).
3854
3855 One of identities might be None, meaning not configured.
3856 """
3857 host_to_identity_pairs = {}
3858 for host, identity, _ in self.get_hosts_with_creds():
3859 canonical = self._canonical_git_googlesource_host(host)
3860 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3861 idx = 0 if canonical == host else 1
3862 pair[idx] = identity
3863 return host_to_identity_pairs
3864
3865 def get_partially_configured_hosts(self):
3866 return set(
Andrii Shyshkalovc8173822017-07-10 10:10:533867 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3868 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3869 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 15:04:323870
3871 def get_conflicting_hosts(self):
3872 return set(
Andrii Shyshkalovc8173822017-07-10 10:10:533873 host
3874 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 15:04:323875 if None not in (i1, i2) and i1 != i2)
3876
3877 def get_duplicated_hosts(self):
3878 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3879 return set(host for host, count in counters.iteritems() if count > 1)
3880
3881 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3882 'chromium.googlesource.com': 'chromium.org',
3883 'chrome-internal.googlesource.com': 'google.com',
3884 }
3885
3886 def get_hosts_with_wrong_identities(self):
3887 """Finds hosts which **likely** reference wrong identities.
3888
3889 Note: skips hosts which have conflicting identities for Git and Gerrit.
3890 """
3891 hosts = set()
3892 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3893 pair = self._get_git_gerrit_identity_pairs().get(host)
3894 if pair and pair[0] == pair[1]:
3895 _, domain = self._parse_identity(pair[0])
3896 if domain != expected:
3897 hosts.add(host)
3898 return hosts
3899
Andrii Shyshkalov0a0b0672017-03-16 15:27:483900 @staticmethod
Andrii Shyshkalovc8173822017-07-10 10:10:533901 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 15:27:483902 hosts = sorted(hosts)
3903 assert hosts
3904 if extra_column_func is None:
3905 extras = [''] * len(hosts)
3906 else:
3907 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 10:10:533908 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3909 lines = []
Andrii Shyshkalov0a0b0672017-03-16 15:27:483910 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 10:10:533911 lines.append(tmpl % he)
3912 return lines
Andrii Shyshkalov0a0b0672017-03-16 15:27:483913
Andrii Shyshkalovc8173822017-07-10 10:10:533914 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 15:27:483915 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 10:10:533916 yield ('.googlesource.com wildcard record detected',
3917 ['Chrome Infrastructure team recommends to list full host names '
3918 'explicitly.'],
3919 None)
Andrii Shyshkalov0a0b0672017-03-16 15:27:483920
3921 dups = self.get_duplicated_hosts()
3922 if dups:
Andrii Shyshkalovc8173822017-07-10 10:10:533923 yield ('The following hosts were defined twice',
3924 self._format_hosts(dups),
3925 None)
Andrii Shyshkalov0a0b0672017-03-16 15:27:483926
3927 partial = self.get_partially_configured_hosts()
3928 if partial:
Andrii Shyshkalovc8173822017-07-10 10:10:533929 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3930 'These hosts are missing',
3931 self._format_hosts(partial, lambda host: 'but %s defined' %
3932 self._get_counterpart_host(host)),
3933 partial)
Andrii Shyshkalov0a0b0672017-03-16 15:27:483934
3935 conflicting = self.get_conflicting_hosts()
3936 if conflicting:
Andrii Shyshkalovc8173822017-07-10 10:10:533937 yield ('The following Git hosts have differing credentials from their '
3938 'Gerrit counterparts',
3939 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3940 tuple(self._get_git_gerrit_identity_pairs()[host])),
3941 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 15:27:483942
3943 wrong = self.get_hosts_with_wrong_identities()
3944 if wrong:
Andrii Shyshkalovc8173822017-07-10 10:10:533945 yield ('These hosts likely use wrong identity',
3946 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3947 (self._get_git_gerrit_identity_pairs()[host][0],
3948 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3949 wrong)
3950
3951 def find_and_report_problems(self):
3952 """Returns True if there was at least one problem, else False."""
3953 found = False
3954 bad_hosts = set()
3955 for title, sublines, hosts in self._find_problems():
3956 if not found:
3957 found = True
3958 print('\n\n.gitcookies problem report:\n')
3959 bad_hosts.update(hosts or [])
3960 print(' %s%s' % (title , (':' if sublines else '')))
3961 if sublines:
3962 print()
3963 print(' %s' % '\n '.join(sublines))
3964 print()
3965
3966 if bad_hosts:
3967 assert found
3968 print(' You can manually remove corresponding lines in your %s file and '
3969 'visit the following URLs with correct account to generate '
3970 'correct credential lines:\n' %
3971 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3972 print(' %s' % '\n '.join(sorted(set(
3973 gerrit_util.CookiesAuthenticator().get_new_password_url(
3974 self._canonical_git_googlesource_host(host))
3975 for host in bad_hosts
3976 ))))
3977 return found
Andrii Shyshkalov0a0b0672017-03-16 15:27:483978
Andrii Shyshkalov353637c2017-03-14 15:52:183979
Edward Lemur5ba1e9c2018-07-23 18:19:023980@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 15:52:183981def CMDcreds_check(parser, args):
3982 """Checks credentials and suggests changes."""
3983 _, _ = parser.parse_args(args)
3984
Vadim Shtayurab250ec12018-10-04 00:21:083985 # Code below checks .gitcookies. Abort if using something else.
3986 authn = gerrit_util.Authenticator.get()
3987 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3988 if isinstance(authn, gerrit_util.GceAuthenticator):
3989 DieWithError(
3990 'This command is not designed for GCE, are you on a bot?\n'
3991 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3992 'in your env.')
Aaron Gabled10ca0e2017-09-11 18:24:103993 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:083994 'This command is not designed for bot environment. It checks '
3995 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 15:52:183996
Andrii Shyshkalov517948b2017-03-15 14:51:593997 checker = _GitCookiesChecker()
3998 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 16:08:323999
Andrii Shyshkalov0a0b0672017-03-16 15:27:484000 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 16:08:324001 checker.print_current_creds(include_netrc=True)
4002
Andrii Shyshkalov0a0b0672017-03-16 15:27:484003 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 20:39:424004 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 15:27:484005 return 0
4006 return 1
Andrii Shyshkalov353637c2017-03-14 15:52:184007
4008
[email protected]0633fb42013-08-16 20:06:144009@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:024010@metrics.collector.collect_metrics('git cl config')
[email protected]cc51cd02010-12-23 00:48:394011def CMDconfig(parser, args):
[email protected]d9c1b202013-07-24 23:52:114012 """Edits configuration for this tree."""
[email protected]cc51cd02010-12-23 00:48:394013
Quinten Yearsley0c62da92017-05-31 20:39:424014 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 13:24:354015 # TODO(tandrii): remove this once we switch to Gerrit.
4016 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
[email protected]87884cc2014-01-03 22:23:414017 parser.add_option('--activate-update', action='store_true',
4018 help='activate auto-updating [rietveld] section in '
4019 '.git/config')
4020 parser.add_option('--deactivate-update', action='store_true',
4021 help='deactivate auto-updating [rietveld] section in '
4022 '.git/config')
4023 options, args = parser.parse_args(args)
4024
4025 if options.deactivate_update:
4026 RunGit(['config', 'rietveld.autoupdate', 'false'])
4027 return
4028
4029 if options.activate_update:
4030 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4031 return
4032
[email protected]cc51cd02010-12-23 00:48:394033 if len(args) == 0:
[email protected]e7d3d162016-03-15 14:15:574034 GetRietveldCodereviewSettingsInteractively()
[email protected]cc51cd02010-12-23 00:48:394035 return 0
4036
4037 url = args[0]
4038 if not url.endswith('codereview.settings'):
4039 url = os.path.join(url, 'codereview.settings')
4040
4041 # Load code review settings and download hooks (if available).
4042 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4043 return 0
4044
4045
Edward Lemur5ba1e9c2018-07-23 18:19:024046@metrics.collector.collect_metrics('git cl baseurl')
[email protected]6b0051e2012-04-03 15:45:084047def CMDbaseurl(parser, args):
[email protected]d9c1b202013-07-24 23:52:114048 """Gets or sets base-url for this branch."""
[email protected]6b0051e2012-04-03 15:45:084049 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4050 branch = ShortBranchName(branchref)
4051 _, args = parser.parse_args(args)
4052 if not args:
vapiera7fbd5a2016-06-16 16:17:494053 print('Current base-url:')
[email protected]6b0051e2012-04-03 15:45:084054 return RunGit(['config', 'branch.%s.base-url' % branch],
4055 error_ok=False).strip()
4056 else:
vapiera7fbd5a2016-06-16 16:17:494057 print('Setting base-url to %s' % args[0])
[email protected]6b0051e2012-04-03 15:45:084058 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4059 error_ok=False).strip()
4060
4061
[email protected]b99fbd92014-09-11 17:29:284062def color_for_status(status):
4063 """Maps a Changelist status to color, for CMDstatus and other tools."""
4064 return {
Aaron Gable9ab38c62017-04-06 21:36:334065 'unsent': Fore.YELLOW,
[email protected]b99fbd92014-09-11 17:29:284066 'waiting': Fore.BLUE,
4067 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 21:36:334068 'not lgtm': Fore.RED,
[email protected]b99fbd92014-09-11 17:29:284069 'lgtm': Fore.GREEN,
4070 'commit': Fore.MAGENTA,
4071 'closed': Fore.CYAN,
4072 'error': Fore.WHITE,
4073 }.get(status, Fore.WHITE)
4074
[email protected]04ea8462016-04-25 19:51:214075
[email protected]cbd7dc32016-05-31 10:33:504076def get_cl_statuses(changes, fine_grained, max_processes=None):
4077 """Returns a blocking iterable of (cl, status) for given branches.
[email protected]ffde55c2015-03-12 00:44:174078
4079 If fine_grained is true, this will fetch CL statuses from the server.
4080 Otherwise, simply indicate if there's a matching url for the given branches.
4081
4082 If max_processes is specified, it is used as the maximum number of processes
4083 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4084 spawned.
[email protected]cf197482016-04-29 20:15:534085
4086 See GetStatus() for a list of possible statuses.
[email protected]ffde55c2015-03-12 00:44:174087 """
qyearsley12fa6ff2016-08-24 16:18:404088 # Silence upload.py otherwise it becomes unwieldy.
[email protected]ffde55c2015-03-12 00:44:174089 upload.verbosity = 0
4090
Andrii Shyshkalov2f8e9242017-01-23 18:20:194091 if not changes:
4092 raise StopIteration()
[email protected]cf197482016-04-29 20:15:534093
Andrii Shyshkalov2f8e9242017-01-23 18:20:194094 if not fine_grained:
4095 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 23:38:184096 # Do not use get_approving_reviewers(), since it requires an HTTP request.
[email protected]cbd7dc32016-05-31 10:33:504097 for cl in changes:
4098 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 18:20:194099 return
4100
4101 # First, sort out authentication issues.
4102 logging.debug('ensuring credentials exist')
4103 for cl in changes:
4104 cl.EnsureAuthenticated(force=False, refresh=True)
4105
4106 def fetch(cl):
4107 try:
4108 return (cl, cl.GetStatus())
4109 except:
4110 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 18:37:154111 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 18:20:194112 raise
4113
4114 threads_count = len(changes)
4115 if max_processes:
4116 threads_count = max(1, min(threads_count, max_processes))
4117 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4118
4119 pool = ThreadPool(threads_count)
4120 fetched_cls = set()
4121 try:
4122 it = pool.imap_unordered(fetch, changes).__iter__()
4123 while True:
4124 try:
4125 cl, status = it.next(timeout=5)
4126 except multiprocessing.TimeoutError:
4127 break
4128 fetched_cls.add(cl)
4129 yield cl, status
4130 finally:
4131 pool.close()
4132
4133 # Add any branches that failed to fetch.
4134 for cl in set(changes) - fetched_cls:
4135 yield (cl, 'error')
[email protected]b99fbd92014-09-11 17:29:284136
[email protected]2dd99862015-06-22 12:22:184137
4138def upload_branch_deps(cl, args):
4139 """Uploads CLs of local branches that are dependents of the current branch.
4140
4141 If the local branch dependency tree looks like:
4142 test1 -> test2.1 -> test3.1
4143 -> test3.2
4144 -> test2.2 -> test3.3
4145
4146 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4147 run on the dependent branches in this order:
4148 test2.1, test3.1, test3.2, test2.2, test3.3
4149
4150 Note: This function does not rebase your local dependent branches. Use it when
4151 you make a change to the parent branch that will not conflict with its
4152 dependent branches, and you would like their dependencies updated in
4153 Rietveld.
4154 """
4155 if git_common.is_dirty_git_tree('upload-branch-deps'):
4156 return 1
4157
4158 root_branch = cl.GetBranch()
4159 if root_branch is None:
4160 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4161 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:234162 if not cl.GetIssue():
[email protected]2dd99862015-06-22 12:22:184163 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4164 'patchset dependencies without an uploaded CL.')
4165
4166 branches = RunGit(['for-each-ref',
4167 '--format=%(refname:short) %(upstream:short)',
4168 'refs/heads'])
4169 if not branches:
4170 print('No local branches found.')
4171 return 0
4172
4173 # Create a dictionary of all local branches to the branches that are dependent
4174 # on it.
4175 tracked_to_dependents = collections.defaultdict(list)
4176 for b in branches.splitlines():
4177 tokens = b.split()
4178 if len(tokens) == 2:
4179 branch_name, tracked = tokens
4180 tracked_to_dependents[tracked].append(branch_name)
4181
vapiera7fbd5a2016-06-16 16:17:494182 print()
4183 print('The dependent local branches of %s are:' % root_branch)
[email protected]2dd99862015-06-22 12:22:184184 dependents = []
4185 def traverse_dependents_preorder(branch, padding=''):
4186 dependents_to_process = tracked_to_dependents.get(branch, [])
4187 padding += ' '
4188 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 16:17:494189 print('%s%s' % (padding, dependent))
[email protected]2dd99862015-06-22 12:22:184190 dependents.append(dependent)
4191 traverse_dependents_preorder(dependent, padding)
4192 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 16:17:494193 print()
[email protected]2dd99862015-06-22 12:22:184194
4195 if not dependents:
vapiera7fbd5a2016-06-16 16:17:494196 print('There are no dependent local branches for %s' % root_branch)
[email protected]2dd99862015-06-22 12:22:184197 return 0
4198
Andrii Shyshkalovabc26ac2017-03-14 13:49:384199 confirm_or_exit('This command will checkout all dependent branches and run '
4200 '"git cl upload".', action='continue')
[email protected]2dd99862015-06-22 12:22:184201
[email protected]2dd99862015-06-22 12:22:184202 # Record all dependents that failed to upload.
4203 failures = {}
4204 # Go through all dependents, checkout the branch and upload.
4205 try:
4206 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 16:17:494207 print()
4208 print('--------------------------------------')
4209 print('Running "git cl upload" from %s:' % dependent_branch)
[email protected]2dd99862015-06-22 12:22:184210 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 16:17:494211 print()
[email protected]2dd99862015-06-22 12:22:184212 try:
4213 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 16:17:494214 print('Upload failed for %s!' % dependent_branch)
[email protected]2dd99862015-06-22 12:22:184215 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 21:53:264216 except: # pylint: disable=bare-except
[email protected]2dd99862015-06-22 12:22:184217 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 16:17:494218 print()
[email protected]2dd99862015-06-22 12:22:184219 finally:
4220 # Swap back to the original root branch.
4221 RunGit(['checkout', '-q', root_branch])
4222
vapiera7fbd5a2016-06-16 16:17:494223 print()
4224 print('Upload complete for dependent branches!')
[email protected]2dd99862015-06-22 12:22:184225 for dependent_branch in dependents:
4226 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 16:17:494227 print(' %s : %s' % (dependent_branch, upload_status))
4228 print()
[email protected]2dd99862015-06-22 12:22:184229
4230 return 0
4231
4232
Edward Lemur5ba1e9c2018-07-23 18:19:024233@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-07 01:31:474234def CMDarchive(parser, args):
4235 """Archives and deletes branches associated with closed changelists."""
4236 parser.add_option(
4237 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 19:02:164238 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-07 01:31:474239 parser.add_option(
4240 '-f', '--force', action='store_true',
4241 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 19:02:164242 parser.add_option(
4243 '-d', '--dry-run', action='store_true',
4244 help='Skip the branch tagging and removal steps.')
4245 parser.add_option(
4246 '-t', '--notags', action='store_true',
4247 help='Do not tag archived branches. '
4248 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-07 01:31:474249
4250 auth.add_auth_options(parser)
4251 options, args = parser.parse_args(args)
4252 if args:
4253 parser.error('Unsupported args: %s' % ' '.join(args))
4254 auth_config = auth.extract_auth_config_from_options(options)
4255
4256 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4257 if not branches:
4258 return 0
4259
vapiera7fbd5a2016-06-16 16:17:494260 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-07 01:31:474261 changes = [Changelist(branchref=b, auth_config=auth_config)
4262 for b in branches.splitlines()]
4263 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4264 statuses = get_cl_statuses(changes,
4265 fine_grained=True,
4266 max_processes=options.maxjobs)
4267 proposal = [(cl.GetBranch(),
4268 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4269 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:584270 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-07 01:31:474271 proposal.sort()
4272
4273 if not proposal:
vapiera7fbd5a2016-06-16 16:17:494274 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-07 01:31:474275 return 0
4276
4277 current_branch = GetCurrentBranch()
4278
vapiera7fbd5a2016-06-16 16:17:494279 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 19:02:164280 if options.notags:
4281 for next_item in proposal:
4282 print(' ' + next_item[0])
4283 else:
4284 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4285 for next_item in proposal:
4286 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-07 01:31:474287
kmarshall9249e012016-08-23 19:02:164288 # Quit now on precondition failure or if instructed by the user, either
4289 # via an interactive prompt or by command line flags.
4290 if options.dry_run:
4291 print('\nNo changes were made (dry run).\n')
4292 return 0
4293 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-07 01:31:474294 print('You are currently on a branch \'%s\' which is associated with a '
4295 'closed codereview issue, so archive cannot proceed. Please '
4296 'checkout another branch and run this command again.' %
4297 current_branch)
4298 return 1
kmarshall9249e012016-08-23 19:02:164299 elif not options.force:
sergiyb4a5ecbe2016-06-20 16:46:004300 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4301 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 16:17:494302 print('Aborted.')
kmarshall3bff56b2016-06-07 01:31:474303 return 1
4304
4305 for branch, tagname in proposal:
kmarshall9249e012016-08-23 19:02:164306 if not options.notags:
4307 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-07 01:31:474308 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 19:02:164309
vapiera7fbd5a2016-06-16 16:17:494310 print('\nJob\'s done!')
kmarshall3bff56b2016-06-07 01:31:474311
4312 return 0
4313
4314
Edward Lemur5ba1e9c2018-07-23 18:19:024315@metrics.collector.collect_metrics('git cl status')
[email protected]cc51cd02010-12-23 00:48:394316def CMDstatus(parser, args):
[email protected]d9c1b202013-07-24 23:52:114317 """Show status of changelists.
4318
4319 Colors are used to tell the state of the CL unless --fast is used:
[email protected]aeab41a2013-12-10 20:01:224320 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 21:36:334321 - Yellow waiting for you to reply to review, or not yet sent
[email protected]aeab41a2013-12-10 20:01:224322 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 21:36:334323 - Red 'not LGTM'ed
[email protected]aeab41a2013-12-10 20:01:224324 - Magenta in the commit queue
4325 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 21:36:334326 - White error, or unknown status
[email protected]d9c1b202013-07-24 23:52:114327
4328 Also see 'git cl comments'.
4329 """
[email protected]cc51cd02010-12-23 00:48:394330 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 15:21:064331 help='print only specific field (desc|id|patch|status|url)')
[email protected]1033efd2013-07-23 23:25:094332 parser.add_option('-f', '--fast', action='store_true',
4333 help='Do not retrieve review status')
[email protected]ffde55c2015-03-12 00:44:174334 parser.add_option(
4335 '-j', '--maxjobs', action='store', type=int,
4336 help='The maximum number of jobs to use when retrieving review status')
[email protected]cf6a5d22015-04-09 22:02:004337
4338 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 21:40:404339 _add_codereview_issue_select_options(
4340 parser, 'Must be in conjunction with --field.')
[email protected]cf6a5d22015-04-09 22:02:004341 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 21:40:404342 _process_codereview_issue_select_options(parser, options)
[email protected]39c0b222013-08-17 16:57:014343 if args:
4344 parser.error('Unsupported args: %s' % args)
[email protected]cf6a5d22015-04-09 22:02:004345 auth_config = auth.extract_auth_config_from_options(options)
[email protected]cc51cd02010-12-23 00:48:394346
iannuccie53c9352016-08-17 21:40:404347 if options.issue is not None and not options.field:
4348 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 20:24:104349
[email protected]cc51cd02010-12-23 00:48:394350 if options.field:
iannucci3c972b92016-08-17 20:24:104351 cl = Changelist(auth_config=auth_config, issue=options.issue,
4352 codereview=options.forced_codereview)
[email protected]cc51cd02010-12-23 00:48:394353 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 16:17:494354 print(cl.GetDescription())
[email protected]cc51cd02010-12-23 00:48:394355 elif options.field == 'id':
4356 issueid = cl.GetIssue()
4357 if issueid:
vapiera7fbd5a2016-06-16 16:17:494358 print(issueid)
[email protected]cc51cd02010-12-23 00:48:394359 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 20:41:464360 patchset = cl.GetMostRecentPatchset()
[email protected]cc51cd02010-12-23 00:48:394361 if patchset:
vapiera7fbd5a2016-06-16 16:17:494362 print(patchset)
phajdan.jr289d03e2016-08-16 15:21:064363 elif options.field == 'status':
4364 print(cl.GetStatus())
[email protected]cc51cd02010-12-23 00:48:394365 elif options.field == 'url':
4366 url = cl.GetIssueURL()
4367 if url:
vapiera7fbd5a2016-06-16 16:17:494368 print(url)
[email protected]e25c75b2013-07-23 18:30:564369 return 0
4370
4371 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4372 if not branches:
4373 print('No local branch found.')
4374 return 0
4375
[email protected]cbd7dc32016-05-31 10:33:504376 changes = [
[email protected]cf6a5d22015-04-09 22:02:004377 Changelist(branchref=b, auth_config=auth_config)
[email protected]cbd7dc32016-05-31 10:33:504378 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 16:17:494379 print('Branches associated with reviews:')
[email protected]cbd7dc32016-05-31 10:33:504380 output = get_cl_statuses(changes,
[email protected]ffde55c2015-03-12 00:44:174381 fine_grained=not options.fast,
[email protected]cbd7dc32016-05-31 10:33:504382 max_processes=options.maxjobs)
[email protected]1033efd2013-07-23 23:25:094383
[email protected]ffde55c2015-03-12 00:44:174384 branch_statuses = {}
[email protected]cbd7dc32016-05-31 10:33:504385 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4386 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4387 branch = cl.GetBranch()
[email protected]ffde55c2015-03-12 00:44:174388 while branch not in branch_statuses:
[email protected]cbd7dc32016-05-31 10:33:504389 c, status = output.next()
4390 branch_statuses[c.GetBranch()] = status
4391 status = branch_statuses.pop(branch)
4392 url = cl.GetIssueURL()
4393 if url and (not status or status == 'error'):
4394 # The issue probably doesn't exist anymore.
4395 url += ' (broken)'
4396
[email protected]a6de1f42015-06-10 04:23:174397 color = color_for_status(status)
[email protected]885f6512013-07-27 02:17:264398 reset = Fore.RESET
[email protected]596cd5c2016-04-04 21:34:394399 if not setup_color.IS_TTY:
[email protected]885f6512013-07-27 02:17:264400 color = ''
4401 reset = ''
[email protected]a6de1f42015-06-10 04:23:174402 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 16:17:494403 print(' %*s : %s%s %s%s' % (
[email protected]cbd7dc32016-05-31 10:33:504404 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 16:17:494405 status_str, reset))
[email protected]1033efd2013-07-23 23:25:094406
Andrii Shyshkalovd0e1d9d2017-01-24 16:10:514407
4408 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 16:17:494409 print()
Andrii Shyshkalovd0e1d9d2017-01-24 16:10:514410 print('Current branch: %s' % branch)
4411 for cl in changes:
4412 if cl.GetBranch() == branch:
4413 break
[email protected]ee87f582015-07-31 18:46:254414 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 16:17:494415 print('No issue assigned.')
[email protected]ee87f582015-07-31 18:46:254416 return 0
vapiera7fbd5a2016-06-16 16:17:494417 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
[email protected]85616e02014-07-28 15:37:554418 if not options.fast:
vapiera7fbd5a2016-06-16 16:17:494419 print('Issue description:')
4420 print(cl.GetDescription(pretty=True))
[email protected]cc51cd02010-12-23 00:48:394421 return 0
4422
4423
[email protected]39c0b222013-08-17 16:57:014424def colorize_CMDstatus_doc():
4425 """To be called once in main() to add colors to git cl status help."""
4426 colors = [i for i in dir(Fore) if i[0].isupper()]
4427
4428 def colorize_line(line):
4429 for color in colors:
4430 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 20:39:424431 # Extract whitespace first and the leading '-'.
[email protected]39c0b222013-08-17 16:57:014432 indent = len(line) - len(line.lstrip(' ')) + 1
4433 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4434 return line
4435
4436 lines = CMDstatus.__doc__.splitlines()
4437 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4438
4439
phajdan.jre328cf92016-08-22 11:12:174440def write_json(path, contents):
Stefan Zager1306bd02017-06-23 02:26:464441 if path == '-':
4442 json.dump(contents, sys.stdout)
4443 else:
4444 with open(path, 'w') as f:
4445 json.dump(contents, f)
phajdan.jre328cf92016-08-22 11:12:174446
4447
[email protected]0633fb42013-08-16 20:06:144448@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:024449@metrics.collector.collect_metrics('git cl issue')
[email protected]cc51cd02010-12-23 00:48:394450def CMDissue(parser, args):
[email protected]d9c1b202013-07-24 23:52:114451 """Sets or displays the current code review issue number.
[email protected]cc51cd02010-12-23 00:48:394452
4453 Pass issue number 0 to clear the current issue.
[email protected]d9c1b202013-07-24 23:52:114454 """
[email protected]406c4402015-03-03 17:22:284455 parser.add_option('-r', '--reverse', action='store_true',
4456 help='Lookup the branch(es) for the specified issues. If '
4457 'no issues are specified, all branches with mapped '
4458 'issues will be listed.')
Stefan Zager1306bd02017-06-23 02:26:464459 parser.add_option('--json',
4460 help='Path to JSON output file, or "-" for stdout.')
[email protected]dde64622016-04-13 17:11:214461 _add_codereview_select_options(parser)
[email protected]406c4402015-03-03 17:22:284462 options, args = parser.parse_args(args)
[email protected]dde64622016-04-13 17:11:214463 _process_codereview_select_options(parser, options)
[email protected]cc51cd02010-12-23 00:48:394464
[email protected]406c4402015-03-03 17:22:284465 if options.reverse:
4466 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 17:49:134467 '--format=%(refname)']).splitlines()
[email protected]406c4402015-03-03 17:22:284468 # Reverse issue lookup.
4469 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:034470
4471 git_config = {}
4472 for config in RunGit(['config', '--get-regexp',
4473 r'branch\..*issue']).splitlines():
4474 name, _space, val = config.partition(' ')
4475 git_config[name] = val
4476
[email protected]406c4402015-03-03 17:22:284477 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:034478 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4479 config_key = _git_branch_config_key(ShortBranchName(branch),
4480 cls.IssueConfigKey())
4481 issue = git_config.get(config_key)
4482 if issue:
4483 issue_branch_map.setdefault(int(issue), []).append(branch)
[email protected]406c4402015-03-03 17:22:284484 if not args:
4485 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 11:12:174486 result = {}
[email protected]406c4402015-03-03 17:22:284487 for issue in args:
4488 if not issue:
4489 continue
phajdan.jre328cf92016-08-22 11:12:174490 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 16:17:494491 print('Branch for issue number %s: %s' % (
4492 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 11:12:174493 if options.json:
4494 write_json(options.json, result)
Aaron Gable78753da2017-06-15 17:35:494495 return 0
4496
4497 if len(args) > 0:
4498 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4499 if not issue.valid:
4500 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4501 'or no argument to list it.\n'
4502 'Maybe you want to run git cl status?')
4503 cl = Changelist(codereview=issue.codereview)
4504 cl.SetIssue(issue.issue)
[email protected]406c4402015-03-03 17:22:284505 else:
[email protected]dde64622016-04-13 17:11:214506 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 17:35:494507 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4508 if options.json:
4509 write_json(options.json, {
4510 'issue': cl.GetIssue(),
4511 'issue_url': cl.GetIssueURL(),
4512 })
[email protected]cc51cd02010-12-23 00:48:394513 return 0
4514
4515
Edward Lemur5ba1e9c2018-07-23 18:19:024516@metrics.collector.collect_metrics('git cl comments')
[email protected]9977a2e2012-06-06 22:30:564517def CMDcomments(parser, args):
[email protected]e4efd512014-11-05 09:05:294518 """Shows or posts review comments for any changelist."""
4519 parser.add_option('-a', '--add-comment', dest='comment',
4520 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 21:23:224521 parser.add_option('-i', '--issue', dest='issue',
4522 help='review issue id (defaults to current issue). '
4523 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 20:01:174524 parser.add_option('-m', '--machine-readable', dest='readable',
4525 action='store_false', default=True,
4526 help='output comments in a format compatible with '
4527 'editor parsing')
[email protected]c85ac942015-09-15 16:34:434528 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-23 02:26:464529 help='File to write JSON summary to, or "-" for stdout')
[email protected]cf6a5d22015-04-09 22:02:004530 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-15 23:24:374531 _add_codereview_select_options(parser)
[email protected]e4efd512014-11-05 09:05:294532 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-15 23:24:374533 _process_codereview_select_options(parser, options)
[email protected]cf6a5d22015-04-09 22:02:004534 auth_config = auth.extract_auth_config_from_options(options)
[email protected]9977a2e2012-06-06 22:30:564535
[email protected]e4efd512014-11-05 09:05:294536 issue = None
4537 if options.issue:
4538 try:
4539 issue = int(options.issue)
4540 except ValueError:
4541 DieWithError('A review issue id is expected to be a number')
[email protected]e4efd512014-11-05 09:05:294542
Andrii Shyshkalov642641d2018-10-16 05:54:414543 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4544
4545 if not cl.IsGerrit():
4546 parser.error('rietveld is not supported')
[email protected]e4efd512014-11-05 09:05:294547
4548 if options.comment:
4549 cl.AddComment(options.comment)
4550 return 0
4551
Aaron Gable0ffdf2d2017-06-05 20:01:174552 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4553 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 15:05:494554 for comment in summary:
4555 if comment.disapproval:
[email protected]e4efd512014-11-05 09:05:294556 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 15:05:494557 elif comment.approval:
[email protected]e4efd512014-11-05 09:05:294558 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 15:05:494559 elif comment.sender == cl.GetIssueOwner():
[email protected]e4efd512014-11-05 09:05:294560 color = Fore.MAGENTA
4561 else:
4562 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 15:05:494563 print('\n%s%s %s%s\n%s' % (
4564 color,
4565 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4566 comment.sender,
4567 Fore.RESET,
4568 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4569
[email protected]c85ac942015-09-15 16:34:434570 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 15:05:494571 def pre_serialize(c):
4572 dct = c.__dict__.copy()
4573 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4574 return dct
Leszek Swirski45b20c42018-09-17 17:05:264575 write_json(options.json_file, map(pre_serialize, summary))
[email protected]9977a2e2012-06-06 22:30:564576 return 0
4577
4578
[email protected]2b55fe32016-04-26 20:28:544579@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:024580@metrics.collector.collect_metrics('git cl description')
[email protected]eec76592013-05-20 16:27:574581def CMDdescription(parser, args):
[email protected]d9c1b202013-07-24 23:52:114582 """Brings up the editor for the current CL's description."""
[email protected]34fb6b12015-07-13 20:03:264583 parser.add_option('-d', '--display', action='store_true',
4584 help='Display the description instead of opening an editor')
[email protected]d6648e22016-04-29 19:22:164585 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 19:37:424586 help='New description to set for this issue (- for stdin, '
4587 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 07:10:474588 parser.add_option('-f', '--force', action='store_true',
4589 help='Delete any unpublished Gerrit edits for this issue '
4590 'without prompting')
[email protected]2b55fe32016-04-26 20:28:544591
4592 _add_codereview_select_options(parser)
[email protected]cf6a5d22015-04-09 22:02:004593 auth.add_auth_options(parser)
[email protected]2b55fe32016-04-26 20:28:544594 options, args = parser.parse_args(args)
4595 _process_codereview_select_options(parser, options)
4596
Andrii Shyshkalov8039be72017-01-26 08:38:184597 target_issue_arg = None
[email protected]2b55fe32016-04-26 20:28:544598 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 11:35:214599 target_issue_arg = ParseIssueNumberArgument(args[0],
4600 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 08:38:184601 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 13:45:094602 parser.error('invalid codereview url or CL id')
[email protected]2b55fe32016-04-26 20:28:544603
martiniss6eda05f2016-06-30 17:18:354604 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:514605 'auth_config': auth.extract_auth_config_from_options(options),
4606 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 17:18:354607 }
Andrii Shyshkalovc9712392017-04-11 11:35:214608 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 08:38:184609 if target_issue_arg:
4610 kwargs['issue'] = target_issue_arg.issue
4611 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 11:35:214612 if target_issue_arg.codereview and not options.forced_codereview:
4613 detected_codereview_from_url = True
4614 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 17:18:354615
4616 cl = Changelist(**kwargs)
[email protected]eec76592013-05-20 16:27:574617 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 11:35:214618 assert not detected_codereview_from_url
[email protected]eec76592013-05-20 16:27:574619 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 11:35:214620
4621 if detected_codereview_from_url:
4622 logging.info('canonical issue/change URL: %s (type: %s)\n',
4623 cl.GetIssueURL(), target_issue_arg.codereview)
4624
[email protected]eec76592013-05-20 16:27:574625 description = ChangeDescription(cl.GetDescription())
[email protected]d6648e22016-04-29 19:22:164626
[email protected]34fb6b12015-07-13 20:03:264627 if options.display:
vapiera7fbd5a2016-06-16 16:17:494628 print(description.description)
[email protected]34fb6b12015-07-13 20:03:264629 return 0
[email protected]d6648e22016-04-29 19:22:164630
4631 if options.new_description:
4632 text = options.new_description
4633 if text == '-':
4634 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 19:37:424635 elif text == '+':
4636 base_branch = cl.GetCommonAncestorWithUpstream()
4637 change = cl.GetChange(base_branch, None, local_description=True)
4638 text = change.FullDescriptionText()
[email protected]d6648e22016-04-29 19:22:164639
4640 description.set_description(text)
4641 else:
Aaron Gable3a16ed12017-03-23 17:51:554642 description.prompt(git_footer=cl.IsGerrit())
[email protected]d6648e22016-04-29 19:22:164643
Andrii Shyshkalov680253d2017-03-15 20:07:364644 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 07:10:474645 cl.UpdateDescription(description.description, force=options.force)
[email protected]eec76592013-05-20 16:27:574646 return 0
4647
4648
Edward Lemur5ba1e9c2018-07-23 18:19:024649@metrics.collector.collect_metrics('git cl lint')
[email protected]44202a22014-03-11 19:22:184650def CMDlint(parser, args):
4651 """Runs cpplint on the current changelist."""
[email protected]f204d4b2014-03-13 07:40:554652 parser.add_option('--filter', action='append', metavar='-x,+y',
4653 help='Comma-separated list of cpplint\'s category-filters')
[email protected]cf6a5d22015-04-09 22:02:004654 auth.add_auth_options(parser)
4655 options, args = parser.parse_args(args)
4656 auth_config = auth.extract_auth_config_from_options(options)
[email protected]44202a22014-03-11 19:22:184657
4658 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 21:53:264659 # pylint: disable=protected-access
[email protected]44202a22014-03-11 19:22:184660 try:
4661 import cpplint
4662 import cpplint_chromium
4663 except ImportError:
vapiera7fbd5a2016-06-16 16:17:494664 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
[email protected]44202a22014-03-11 19:22:184665 return 1
4666
4667 # Change the current working directory before calling lint so that it
4668 # shows the correct base.
4669 previous_cwd = os.getcwd()
4670 os.chdir(settings.GetRoot())
4671 try:
[email protected]cf6a5d22015-04-09 22:02:004672 cl = Changelist(auth_config=auth_config)
[email protected]44202a22014-03-11 19:22:184673 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4674 files = [f.LocalPath() for f in change.AffectedFiles()]
[email protected]5839eb52014-05-30 16:20:514675 if not files:
vapiera7fbd5a2016-06-16 16:17:494676 print('Cannot lint an empty CL')
[email protected]5839eb52014-05-30 16:20:514677 return 1
[email protected]44202a22014-03-11 19:22:184678
4679 # Process cpplints arguments if any.
[email protected]f204d4b2014-03-13 07:40:554680 command = args + files
4681 if options.filter:
4682 command = ['--filter=' + ','.join(options.filter)] + command
4683 filenames = cpplint.ParseArguments(command)
[email protected]44202a22014-03-11 19:22:184684
4685 white_regex = re.compile(settings.GetLintRegex())
4686 black_regex = re.compile(settings.GetLintIgnoreRegex())
4687 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4688 for filename in filenames:
4689 if white_regex.match(filename):
4690 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 16:17:494691 print('Ignoring file %s' % filename)
[email protected]44202a22014-03-11 19:22:184692 else:
4693 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4694 extra_check_functions)
4695 else:
vapiera7fbd5a2016-06-16 16:17:494696 print('Skipping file %s' % filename)
[email protected]44202a22014-03-11 19:22:184697 finally:
4698 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 16:17:494699 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
[email protected]44202a22014-03-11 19:22:184700 if cpplint._cpplint_state.error_count != 0:
4701 return 1
4702 return 0
4703
4704
Edward Lemur5ba1e9c2018-07-23 18:19:024705@metrics.collector.collect_metrics('git cl presubmit')
[email protected]cc51cd02010-12-23 00:48:394706def CMDpresubmit(parser, args):
[email protected]d9c1b202013-07-24 23:52:114707 """Runs presubmit tests on the current changelist."""
[email protected]375a9022013-01-07 01:12:054708 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 18:08:144709 help='Run upload hook instead of the push hook')
[email protected]375a9022013-01-07 01:12:054710 parser.add_option('-f', '--force', action='store_true',
[email protected]495ad152012-09-04 23:07:424711 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 22:39:414712 parser.add_option('--all', action='store_true',
4713 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 22:50:294714 parser.add_option('--parallel', action='store_true',
4715 help='Run all tests specified by input_api.RunTests in all '
4716 'PRESUBMIT files in parallel.')
[email protected]cf6a5d22015-04-09 22:02:004717 auth.add_auth_options(parser)
4718 options, args = parser.parse_args(args)
4719 auth_config = auth.extract_auth_config_from_options(options)
[email protected]cc51cd02010-12-23 00:48:394720
[email protected]71437c02015-04-09 19:29:404721 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 16:17:494722 print('use --force to check even if tree is dirty.')
[email protected]cc51cd02010-12-23 00:48:394723 return 1
4724
[email protected]cf6a5d22015-04-09 22:02:004725 cl = Changelist(auth_config=auth_config)
[email protected]cc51cd02010-12-23 00:48:394726 if args:
4727 base_branch = args[0]
4728 else:
[email protected]0f58fa82012-11-05 01:45:204729 # Default to diffing against the common ancestor of the upstream branch.
[email protected]8b0553c2014-02-11 00:33:374730 base_branch = cl.GetCommonAncestorWithUpstream()
[email protected]cc51cd02010-12-23 00:48:394731
Aaron Gable8076c282017-11-29 22:39:414732 if options.all:
4733 base_change = cl.GetChange(base_branch, None)
4734 files = [('M', f) for f in base_change.AllFiles()]
4735 change = presubmit_support.GitChange(
4736 base_change.Name(),
4737 base_change.FullDescriptionText(),
4738 base_change.RepositoryRoot(),
4739 files,
4740 base_change.issue,
4741 base_change.patchset,
4742 base_change.author_email,
4743 base_change._upstream)
4744 else:
4745 change = cl.GetChange(base_branch, None)
4746
[email protected]051ad0e2013-03-04 21:57:344747 cl.RunHook(
4748 committing=not options.upload,
4749 may_prompt=False,
4750 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 22:50:294751 change=change,
4752 parallel=options.parallel)
[email protected]0a2bb372011-03-25 01:16:224753 return 0
[email protected]cc51cd02010-12-23 00:48:394754
4755
[email protected]65874e12016-03-04 12:03:024756def GenerateGerritChangeId(message):
4757 """Returns Ixxxxxx...xxx change id.
4758
4759 Works the same way as
4760 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4761 but can be called on demand on all platforms.
4762
4763 The basic idea is to generate git hash of a state of the tree, original commit
4764 message, author/committer info and timestamps.
4765 """
4766 lines = []
4767 tree_hash = RunGitSilent(['write-tree'])
4768 lines.append('tree %s' % tree_hash.strip())
4769 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4770 if code == 0:
4771 lines.append('parent %s' % parent.strip())
4772 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4773 lines.append('author %s' % author.strip())
4774 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4775 lines.append('committer %s' % committer.strip())
4776 lines.append('')
4777 # Note: Gerrit's commit-hook actually cleans message of some lines and
4778 # whitespace. This code is not doing this, but it clearly won't decrease
4779 # entropy.
4780 lines.append(message)
4781 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4782 stdin='\n'.join(lines))
4783 return 'I%s' % change_hash.strip()
4784
4785
Andrii Shyshkalovf3a20ae2017-01-24 20:23:574786def GetTargetRef(remote, remote_branch, target_branch):
[email protected]455dc922015-01-26 20:15:504787 """Computes the remote branch ref to use for the CL.
4788
4789 Args:
4790 remote (str): The git remote for the CL.
4791 remote_branch (str): The git remote branch for the CL.
4792 target_branch (str): The target branch specified by the user.
[email protected]455dc922015-01-26 20:15:504793 """
4794 if not (remote and remote_branch):
4795 return None
[email protected]27386dd2015-02-16 10:45:394796
[email protected]455dc922015-01-26 20:15:504797 if target_branch:
Quinten Yearsley0c62da92017-05-31 20:39:424798 # Canonicalize branch references to the equivalent local full symbolic
[email protected]455dc922015-01-26 20:15:504799 # refs, which are then translated into the remote full symbolic refs
4800 # below.
4801 if '/' not in target_branch:
4802 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4803 else:
4804 prefix_replacements = (
4805 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4806 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4807 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4808 )
4809 match = None
4810 for regex, replacement in prefix_replacements:
4811 match = re.search(regex, target_branch)
4812 if match:
4813 remote_branch = target_branch.replace(match.group(0), replacement)
4814 break
4815 if not match:
4816 # This is a branch path but not one we recognize; use as-is.
4817 remote_branch = target_branch
[email protected]c68112d2015-03-03 12:48:064818 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4819 # Handle the refs that need to land in different refs.
4820 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
[email protected]27386dd2015-02-16 10:45:394821
[email protected]455dc922015-01-26 20:15:504822 # Create the true path to the remote branch.
4823 # Does the following translation:
4824 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4825 # * refs/remotes/origin/master -> refs/heads/master
4826 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4827 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4828 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4829 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4830 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4831 'refs/heads/')
4832 elif remote_branch.startswith('refs/remotes/branch-heads'):
4833 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 14:10:134834
[email protected]455dc922015-01-26 20:15:504835 return remote_branch
4836
4837
[email protected]eb52a5c2013-04-10 23:17:094838def cleanup_list(l):
4839 """Fixes a list so that comma separated items are put as individual items.
4840
4841 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4842 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4843 """
4844 items = sum((i.split(',') for i in l), [])
4845 stripped_items = (i.strip() for i in items)
4846 return sorted(filter(None, stripped_items))
4847
4848
Aaron Gable4db38df2017-11-03 21:59:074849@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:024850@metrics.collector.collect_metrics('git cl upload')
[email protected]e8077812012-02-03 03:41:464851def CMDupload(parser, args):
[email protected]78948ed2015-07-08 23:09:574852 """Uploads the current changelist to codereview.
4853
4854 Can skip dependency patchset uploads for a branch by running:
4855 git config branch.branch_name.skip-deps-uploads True
4856 To unset run:
4857 git config --unset branch.branch_name.skip-deps-uploads
4858 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 07:17:284859
4860 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4861 a bug number, this bug number is automatically populated in the CL
4862 description.
Nodir Turakulovd0e2cd22017-11-15 18:22:064863
4864 If subject contains text in square brackets or has "<text>: " prefix, such
4865 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4866 [git-cl] add support for hashtags
4867 Foo bar: implement foo
4868 will be hashtagged with "git-cl" and "foo-bar" respectively.
[email protected]78948ed2015-07-08 23:09:574869 """
[email protected]e8077812012-02-03 03:41:464870 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4871 help='bypass upload presubmit hook')
[email protected]b65c43c2013-06-10 22:04:494872 parser.add_option('--bypass-watchlists', action='store_true',
4873 dest='bypass_watchlists',
4874 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 21:26:314875 parser.add_option('-f', '--force', action='store_true', dest='force',
[email protected]e8077812012-02-03 03:41:464876 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 18:08:144877 parser.add_option('--message', '-m', dest='message',
4878 help='message for patchset')
tandriif9aefb72016-07-01 16:06:514879 parser.add_option('-b', '--bug',
4880 help='pre-populate the bug number(s) for this issue. '
4881 'If several, separate with commas')
tandriib80458a2016-06-23 19:20:074882 parser.add_option('--message-file', dest='message_file',
4883 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 18:08:144884 parser.add_option('--title', '-t', dest='title',
4885 help='title for patchset')
[email protected]e8077812012-02-03 03:41:464886 parser.add_option('-r', '--reviewers',
[email protected]eb52a5c2013-04-10 23:17:094887 action='append', default=[],
[email protected]e8077812012-02-03 03:41:464888 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 18:38:004889 parser.add_option('--tbrs',
4890 action='append', default=[],
4891 help='TBR email addresses')
[email protected]e8077812012-02-03 03:41:464892 parser.add_option('--cc',
[email protected]eb52a5c2013-04-10 23:17:094893 action='append', default=[],
[email protected]e8077812012-02-03 03:41:464894 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 18:22:064895 parser.add_option('--hashtag', dest='hashtags',
4896 action='append', default=[],
4897 help=('Gerrit hashtag for new CL; '
4898 'can be applied multiple times'))
[email protected]36f47302013-04-05 01:08:314899 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 18:54:464900 help='send email to reviewer(s) and cc(s) immediately')
[email protected]e8077812012-02-03 03:41:464901 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 20:22:284902 help='tell the commit queue to commit this patchset; '
4903 'implies --send-mail')
[email protected]8ef7ab22012-11-28 04:24:524904 parser.add_option('--target_branch',
[email protected]b9f27512014-08-08 15:52:334905 '--target-branch',
[email protected]455dc922015-01-26 20:15:504906 metavar='TARGET',
4907 help='Apply CL to remote ref TARGET. ' +
4908 'Default: remote branch head, or master')
[email protected]27386dd2015-02-16 10:45:394909 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 18:22:064910 help='Squash multiple commits into one')
[email protected]54b400c2016-01-14 10:08:254911 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 18:22:064912 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 18:22:434913 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 18:22:064914 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 22:49:024915 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4916 const='TBR', help='add a set of OWNERS to TBR')
4917 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4918 const='R', help='add a set of OWNERS to R')
[email protected]d50452a2015-11-23 16:38:154919 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4920 action='store_true',
[email protected]ef966222015-04-07 11:15:014921 help='Send the patchset to do a CQ dry run right after '
4922 'upload.')
[email protected]2dd99862015-06-22 12:22:184923 parser.add_option('--dependencies', action='store_true',
4924 help='Uploads CLs of all the local branches that depend on '
4925 'the current branch')
Ravi Mistry31e7d562018-04-02 16:53:574926 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4927 help='Sends your change to the CQ after an approval. Only '
4928 'works on repos that have the Auto-Submit label '
4929 'enabled')
Edward Lesmes8e282792018-04-03 22:50:294930 parser.add_option('--parallel', action='store_true',
4931 help='Run all tests specified by input_api.RunTests in all '
4932 'PRESUBMIT files in parallel.')
[email protected]91141372014-01-09 23:27:204933
Sergiy Byelozyorov1aa405f2018-09-18 17:38:434934 parser.add_option('--no-autocc', action='store_true',
4935 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 18:22:064936 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:434937 help='Set the review private. This implies --no-autocc.')
4938
[email protected]2dd99862015-06-22 12:22:184939 orig_args = args
[email protected]cf6a5d22015-04-09 22:02:004940 auth.add_auth_options(parser)
[email protected]dde64622016-04-13 17:11:214941 _add_codereview_select_options(parser)
[email protected]e8077812012-02-03 03:41:464942 (options, args) = parser.parse_args(args)
[email protected]dde64622016-04-13 17:11:214943 _process_codereview_select_options(parser, options)
[email protected]cf6a5d22015-04-09 22:02:004944 auth_config = auth.extract_auth_config_from_options(options)
[email protected]e8077812012-02-03 03:41:464945
[email protected]71437c02015-04-09 19:29:404946 if git_common.is_dirty_git_tree('upload'):
[email protected]e8077812012-02-03 03:41:464947 return 1
4948
[email protected]eb52a5c2013-04-10 23:17:094949 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 18:38:004950 options.tbrs = cleanup_list(options.tbrs)
[email protected]eb52a5c2013-04-10 23:17:094951 options.cc = cleanup_list(options.cc)
4952
tandriib80458a2016-06-23 19:20:074953 if options.message_file:
4954 if options.message:
4955 parser.error('only one of --message and --message-file allowed.')
4956 options.message = gclient_utils.FileRead(options.message_file)
4957 options.message_file = None
4958
tandrii4d0545a2016-07-06 10:56:494959 if options.cq_dry_run and options.use_commit_queue:
4960 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4961
Aaron Gableedbc4132017-09-11 20:22:284962 if options.use_commit_queue:
4963 options.send_mail = True
4964
[email protected]512d79c2016-03-31 12:55:284965 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4966 settings.GetIsGerrit()
4967
[email protected]dde64622016-04-13 17:11:214968 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:234969 if not cl.IsGerrit():
4970 # Error out with instructions for repos not yet configured for Gerrit.
4971 print('=====================================')
4972 print('NOTICE: Rietveld is no longer supported. '
4973 'You can upload changes to Gerrit with')
4974 print(' git cl upload --gerrit')
4975 print('or set Gerrit to be your default code review tool with')
4976 print(' git config gerrit.host true')
4977 print('=====================================')
4978 return 1
4979
[email protected]9e6c3a52016-04-12 14:13:084980 return cl.CMDUpload(options, args, orig_args)
[email protected]e8077812012-02-03 03:41:464981
4982
Francois Dorayd42c6812017-05-30 19:10:204983@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:024984@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 19:10:204985def CMDsplit(parser, args):
4986 """Splits a branch into smaller branches and uploads CLs.
4987
4988 Creates a branch and uploads a CL for each group of files modified in the
4989 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 20:39:424990 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 19:10:204991 the shared OWNERS file.
4992 """
4993 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 21:36:054994 help="A text file containing a CL description in which "
4995 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 19:10:204996 parser.add_option("-c", "--comment", dest="comment_file",
4997 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 00:22:174998 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4999 default=False,
5000 help="List the files and reviewers for each CL that would "
5001 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:305002 parser.add_option("--cq-dry-run", action='store_true',
5003 help="If set, will do a cq dry run for each uploaded CL. "
5004 "Please be careful when doing this; more than ~10 CLs "
5005 "has the potential to overload our build "
5006 "infrastructure. Try to upload these not during high "
5007 "load times (usually 11-3 Mountain View time). Email "
5008 "[email protected] with any questions.")
Francois Dorayd42c6812017-05-30 19:10:205009 options, _ = parser.parse_args(args)
5010
5011 if not options.description_file:
5012 parser.error('No --description flag specified.')
5013
5014 def WrappedCMDupload(args):
5015 return CMDupload(OptionParser(), args)
5016
5017 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:305018 Changelist, WrappedCMDupload, options.dry_run,
5019 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 19:10:205020
5021
Aaron Gable1bc7bfe2016-12-19 18:08:145022@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:025023@metrics.collector.collect_metrics('git cl commit')
[email protected]cc51cd02010-12-23 00:48:395024def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 18:08:145025 """DEPRECATED: Used to commit the current changelist via git-svn."""
5026 message = ('git-cl no longer supports committing to SVN repositories via '
5027 'git-svn. You probably want to use `git cl land` instead.')
5028 print(message)
5029 return 1
[email protected]cc51cd02010-12-23 00:48:395030
5031
Andrii Shyshkalovaa31b972017-03-24 15:16:335032# Two special branches used by git cl land.
5033MERGE_BRANCH = 'git-cl-commit'
5034CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5035
5036
[email protected]0633fb42013-08-16 20:06:145037@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:025038@metrics.collector.collect_metrics('git cl land')
[email protected]cee6dc42014-05-07 17:04:035039def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 18:08:145040 """Commits the current changelist via git.
5041
5042 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5043 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 18:08:145044 """
5045 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5046 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 21:26:315047 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 18:08:145048 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 21:50:525049 parser.add_option('--parallel', action='store_true',
5050 help='Run all tests specified by input_api.RunTests in all '
5051 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 18:08:145052 auth.add_auth_options(parser)
5053 (options, args) = parser.parse_args(args)
5054 auth_config = auth.extract_auth_config_from_options(options)
5055
5056 cl = Changelist(auth_config=auth_config)
5057
Robert Iannucci2e73d432018-03-14 08:10:475058 if not cl.IsGerrit():
5059 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 18:08:145060
Robert Iannucci2e73d432018-03-14 08:10:475061 if not cl.GetIssue():
5062 DieWithError('You must upload the change first to Gerrit.\n'
5063 ' If you would rather have `git cl land` upload '
5064 'automatically for you, see http://crbug.com/642759')
5065 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 08:02:565066 options.verbose, options.parallel)
[email protected]cc51cd02010-12-23 00:48:395067
5068
[email protected]fbed6562015-09-25 21:22:365069@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:025070@metrics.collector.collect_metrics('git cl patch')
[email protected]cc51cd02010-12-23 00:48:395071def CMDpatch(parser, args):
[email protected]e5e59002013-10-02 23:21:255072 """Patches in a code review."""
[email protected]cc51cd02010-12-23 00:48:395073 parser.add_option('-b', dest='newbranch',
5074 help='create a new branch off trunk for the patch')
[email protected]1ef44af2013-10-16 16:24:325075 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 15:22:095076 help='overwrite state on the current or chosen branch')
[email protected]1ef44af2013-10-16 16:24:325077 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 15:22:095078 help='change to the directory DIR immediately, '
[email protected]f86c7d32016-04-01 19:27:305079 'before doing anything else. Rietveld only.')
[email protected]1ef44af2013-10-16 16:24:325080 parser.add_option('--reject', action='store_true',
[email protected]6a0b07c2013-07-10 01:29:195081 help='failed patches spew .rej files rather than '
[email protected]f86c7d32016-04-01 19:27:305082 'attempting a 3-way merge. Rietveld only.')
[email protected]cc51cd02010-12-23 00:48:395083 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
[email protected]f86c7d32016-04-01 19:27:305084 help='don\'t commit after patch applies. Rietveld only.')
[email protected]1d88dd32016-02-04 16:25:125085
[email protected]f86c7d32016-04-01 19:27:305086
5087 group = optparse.OptionGroup(
5088 parser,
5089 'Options for continuing work on the current issue uploaded from a '
5090 'different clone (e.g. different machine). Must be used independently '
5091 'from the other options. No issue number should be specified, and the '
5092 'branch must have an issue number associated with it')
5093 group.add_option('--reapply', action='store_true', dest='reapply',
5094 help='Reset the branch and reapply the issue.\n'
5095 'CAUTION: This will undo any local changes in this '
5096 'branch')
[email protected]1d88dd32016-02-04 16:25:125097
5098 group.add_option('--pull', action='store_true', dest='pull',
[email protected]f86c7d32016-04-01 19:27:305099 help='Performs a pull before reapplying.')
[email protected]1d88dd32016-02-04 16:25:125100 parser.add_option_group(group)
5101
[email protected]cf6a5d22015-04-09 22:02:005102 auth.add_auth_options(parser)
[email protected]dde64622016-04-13 17:11:215103 _add_codereview_select_options(parser)
[email protected]cc51cd02010-12-23 00:48:395104 (options, args) = parser.parse_args(args)
[email protected]dde64622016-04-13 17:11:215105 _process_codereview_select_options(parser, options)
[email protected]cf6a5d22015-04-09 22:02:005106 auth_config = auth.extract_auth_config_from_options(options)
5107
Andrii Shyshkalov18975322017-01-25 15:44:135108 if options.reapply:
[email protected]c2786d92016-05-31 19:53:505109 if options.newbranch:
5110 parser.error('--reapply works on the current branch only')
[email protected]1d88dd32016-02-04 16:25:125111 if len(args) > 0:
[email protected]c2786d92016-05-31 19:53:505112 parser.error('--reapply implies no additional arguments')
[email protected]fbed6562015-09-25 21:22:365113
[email protected]c2786d92016-05-31 19:53:505114 cl = Changelist(auth_config=auth_config,
5115 codereview=options.forced_codereview)
5116 if not cl.GetIssue():
5117 parser.error('current branch must have an associated issue')
5118
[email protected]1d88dd32016-02-04 16:25:125119 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 15:44:135120 if upstream is None:
[email protected]f86c7d32016-04-01 19:27:305121 parser.error('No upstream branch specified. Cannot reset branch')
[email protected]1d88dd32016-02-04 16:25:125122
5123 RunGit(['reset', '--hard', upstream])
5124 if options.pull:
5125 RunGit(['pull'])
[email protected]1d88dd32016-02-04 16:25:125126
[email protected]c2786d92016-05-31 19:53:505127 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5128 options.directory)
5129
5130 if len(args) != 1 or not args[0]:
5131 parser.error('Must specify issue number or url')
5132
Andrii Shyshkalovc9712392017-04-11 11:35:215133 target_issue_arg = ParseIssueNumberArgument(args[0],
5134 options.forced_codereview)
5135 if not target_issue_arg.valid:
5136 parser.error('invalid codereview url or CL id')
5137
5138 cl_kwargs = {
5139 'auth_config': auth_config,
5140 'codereview_host': target_issue_arg.hostname,
5141 'codereview': options.forced_codereview,
5142 }
5143 detected_codereview_from_url = False
5144 if target_issue_arg.codereview and not options.forced_codereview:
5145 detected_codereview_from_url = True
5146 cl_kwargs['codereview'] = target_issue_arg.codereview
5147 cl_kwargs['issue'] = target_issue_arg.issue
5148
[email protected]c2786d92016-05-31 19:53:505149 # We don't want uncommitted changes mixed up with the patch.
5150 if git_common.is_dirty_git_tree('patch'):
[email protected]fbed6562015-09-25 21:22:365151 return 1
[email protected]cc51cd02010-12-23 00:48:395152
[email protected]c2786d92016-05-31 19:53:505153 if options.newbranch:
5154 if options.force:
5155 RunGit(['branch', '-D', options.newbranch],
5156 stderr=subprocess2.PIPE, error_ok=True)
5157 RunGit(['new-branch', options.newbranch])
5158
Andrii Shyshkalovc9712392017-04-11 11:35:215159 cl = Changelist(**cl_kwargs)
[email protected]c2786d92016-05-31 19:53:505160
[email protected]f86c7d32016-04-01 19:27:305161 if cl.IsGerrit():
5162 if options.reject:
5163 parser.error('--reject is not supported with Gerrit codereview.')
[email protected]f86c7d32016-04-01 19:27:305164 if options.directory:
5165 parser.error('--directory is not supported with Gerrit codereview.')
5166
Andrii Shyshkalovc9712392017-04-11 11:35:215167 if detected_codereview_from_url:
5168 print('canonical issue/change URL: %s (type: %s)\n' %
5169 (cl.GetIssueURL(), target_issue_arg.codereview))
5170
5171 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 15:22:095172 options.nocommit, options.directory,
5173 options.force)
[email protected]cc51cd02010-12-23 00:48:395174
5175
[email protected]3ec0d542014-01-14 20:00:035176def GetTreeStatus(url=None):
[email protected]cc51cd02010-12-23 00:48:395177 """Fetches the tree status and returns either 'open', 'closed',
5178 'unknown' or 'unset'."""
[email protected]3ec0d542014-01-14 20:00:035179 url = url or settings.GetTreeStatusUrl(error_ok=True)
[email protected]cc51cd02010-12-23 00:48:395180 if url:
5181 status = urllib2.urlopen(url).read().lower()
5182 if status.find('closed') != -1 or status == '0':
5183 return 'closed'
5184 elif status.find('open') != -1 or status == '1':
5185 return 'open'
5186 return 'unknown'
[email protected]cc51cd02010-12-23 00:48:395187 return 'unset'
5188
[email protected]970c5222011-03-12 00:32:245189
[email protected]cc51cd02010-12-23 00:48:395190def GetTreeStatusReason():
5191 """Fetches the tree status from a json url and returns the message
5192 with the reason for the tree to be opened or closed."""
[email protected]bf1a7ba2011-02-01 16:21:465193 url = settings.GetTreeStatusUrl()
5194 json_url = urlparse.urljoin(url, '/current?format=json')
[email protected]cc51cd02010-12-23 00:48:395195 connection = urllib2.urlopen(json_url)
5196 status = json.loads(connection.read())
5197 connection.close()
5198 return status['message']
5199
[email protected]970c5222011-03-12 00:32:245200
Edward Lemur5ba1e9c2018-07-23 18:19:025201@metrics.collector.collect_metrics('git cl tree')
[email protected]cc51cd02010-12-23 00:48:395202def CMDtree(parser, args):
[email protected]d9c1b202013-07-24 23:52:115203 """Shows the status of the tree."""
[email protected]97ae58e2011-03-18 00:29:205204 _, args = parser.parse_args(args)
[email protected]cc51cd02010-12-23 00:48:395205 status = GetTreeStatus()
5206 if 'unset' == status:
vapiera7fbd5a2016-06-16 16:17:495207 print('You must configure your tree status URL by running "git cl config".')
[email protected]cc51cd02010-12-23 00:48:395208 return 2
5209
vapiera7fbd5a2016-06-16 16:17:495210 print('The tree is %s' % status)
5211 print()
5212 print(GetTreeStatusReason())
[email protected]cc51cd02010-12-23 00:48:395213 if status != 'open':
5214 return 1
5215 return 0
5216
5217
Edward Lemur5ba1e9c2018-07-23 18:19:025218@metrics.collector.collect_metrics('git cl try')
[email protected]15192402012-09-06 12:38:295219def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 20:22:035220 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 07:10:525221 group = optparse.OptionGroup(parser, 'Try job options')
[email protected]15192402012-09-06 12:38:295222 group.add_option(
tandrii1838bad2016-10-06 07:10:525223 '-b', '--bot', action='append',
5224 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5225 'times to specify multiple builders. ex: '
5226 '"-b win_rel -b win_layout". See '
5227 'the try server waterfall for the builders name and the tests '
5228 'available.'))
[email protected]15192402012-09-06 12:38:295229 group.add_option(
borenet6c0efe62016-10-19 15:13:295230 '-B', '--bucket', default='',
5231 help=('Buildbucket bucket to send the try requests.'))
5232 group.add_option(
tandrii1838bad2016-10-06 07:10:525233 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 19:34:445234 help=('DEPRECATED, use -B. The try master where to run the builds.'))
[email protected]58a69cb2014-03-01 02:08:295235 group.add_option(
tandrii1838bad2016-10-06 07:10:525236 '-r', '--revision',
tandriif7b29d42016-10-07 15:45:415237 help='Revision to use for the try job; default: the revision will '
5238 'be determined by the try recipe that builder runs, which usually '
5239 'defaults to HEAD of origin/master')
[email protected]15192402012-09-06 12:38:295240 group.add_option(
tandrii1838bad2016-10-06 07:10:525241 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 15:45:415242 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 07:10:525243 'incremental build')
[email protected]15192402012-09-06 12:38:295244 group.add_option(
Andrii Shyshkalovf9648b52018-02-22 06:32:425245 '--category', default='git_cl_try', help='Specify custom build category.')
5246 group.add_option(
tandrii1838bad2016-10-06 07:10:525247 '--project',
5248 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 15:45:415249 'in recipe to determine to which repository or directory to '
5250 'apply the patch')
[email protected]15192402012-09-06 12:38:295251 group.add_option(
tandrii1838bad2016-10-06 07:10:525252 '-p', '--property', dest='properties', action='append', default=[],
5253 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 15:45:415254 'key2=value2 etc. The value will be treated as '
5255 'json if decodable, or as string otherwise. '
5256 'NOTE: using this may make your try job not usable for CQ, '
5257 'which will then schedule another try job with default properties')
[email protected]db375572015-08-17 19:22:235258 group.add_option(
tandrii1838bad2016-10-06 07:10:525259 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5260 help='Host of buildbucket. The default host is %default.')
[email protected]15192402012-09-06 12:38:295261 parser.add_option_group(group)
[email protected]cf6a5d22015-04-09 22:02:005262 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 08:17:335263 _add_codereview_issue_select_options(parser)
[email protected]15192402012-09-06 12:38:295264 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 08:17:335265 _process_codereview_issue_select_options(parser, options)
[email protected]cf6a5d22015-04-09 22:02:005266 auth_config = auth.extract_auth_config_from_options(options)
[email protected]15192402012-09-06 12:38:295267
Nodir Turakulovf6929a12017-10-09 19:34:445268 if options.master and options.master.startswith('luci.'):
5269 parser.error(
5270 '-m option does not support LUCI. Please pass -B %s' % options.master)
[email protected]45453142015-09-15 08:45:225271 # Make sure that all properties are prop=value pairs.
5272 bad_params = [x for x in options.properties if '=' not in x]
5273 if bad_params:
5274 parser.error('Got properties with missing "=": %s' % bad_params)
5275
[email protected]15192402012-09-06 12:38:295276 if args:
5277 parser.error('Unknown arguments: %s' % args)
5278
Koji Ishii31c14782018-01-08 08:17:335279 cl = Changelist(auth_config=auth_config, issue=options.issue,
5280 codereview=options.forced_codereview)
[email protected]15192402012-09-06 12:38:295281 if not cl.GetIssue():
5282 parser.error('Need to upload first')
5283
Andrii Shyshkaloveadad922017-01-26 08:38:305284 if cl.IsGerrit():
5285 # HACK: warm up Gerrit change detail cache to save on RPCs.
5286 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5287
tandriie113dfd2016-10-11 17:20:125288 error_message = cl.CannotTriggerTryJobReason()
5289 if error_message:
qyearsley99e2cdf2016-10-23 19:51:415290 parser.error('Can\'t trigger try jobs: %s' % error_message)
[email protected]16f10f72014-06-24 22:14:365291
borenet6c0efe62016-10-19 15:13:295292 if options.bucket and options.master:
5293 parser.error('Only one of --bucket and --master may be used.')
5294
qyearsley1fdfcb62016-10-24 20:22:035295 buckets = _get_bucket_map(cl, options, parser)
[email protected]8da7f272014-03-14 01:28:395296
qyearsleydd49f942016-10-28 18:57:225297 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5298 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 20:22:035299 if not buckets:
qyearsley1fdfcb62016-10-24 20:22:035300 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 18:50:525301 print('git cl try with no bots now defaults to CQ dry run.')
5302 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5303 return cl.SetCQState(_CQState.DRY_RUN)
[email protected]43064fd2013-12-18 20:07:445304
borenet6c0efe62016-10-19 15:13:295305 for builders in buckets.itervalues():
[email protected]58a69cb2014-03-01 02:08:295306 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 16:17:495307 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 13:02:305308 'of bot requires an initial job from a parent (usually a builder). '
5309 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 16:17:495310 'Bot list: %s' % builders, file=sys.stderr)
[email protected]58a69cb2014-03-01 02:08:295311 return 1
[email protected]f3b21232012-09-24 20:48:555312
[email protected]36e420b2013-08-06 23:21:125313 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 14:49:185314 try:
Andrii Shyshkalovf9648b52018-02-22 06:32:425315 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 14:49:185316 except BuildbucketResponseException as ex:
5317 print('ERROR: %s' % ex)
5318 return 1
[email protected]15192402012-09-06 12:38:295319 return 0
5320
5321
Edward Lemur5ba1e9c2018-07-23 18:19:025322@metrics.collector.collect_metrics('git cl try-results')
[email protected]b015fac2016-02-26 14:52:015323def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 07:10:525324 """Prints info about try jobs associated with current CL."""
5325 group = optparse.OptionGroup(parser, 'Try job results options')
[email protected]b015fac2016-02-26 14:52:015326 group.add_option(
tandrii1838bad2016-10-06 07:10:525327 '-p', '--patchset', type=int, help='patchset number if not current.')
[email protected]b015fac2016-02-26 14:52:015328 group.add_option(
tandrii1838bad2016-10-06 07:10:525329 '--print-master', action='store_true', help='print master name as well.')
[email protected]6cf98c82016-03-15 11:56:005330 group.add_option(
tandrii1838bad2016-10-06 07:10:525331 '--color', action='store_true', default=setup_color.IS_TTY,
5332 help='force color output, useful when piping output.')
[email protected]b015fac2016-02-26 14:52:015333 group.add_option(
tandrii1838bad2016-10-06 07:10:525334 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5335 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 17:45:135336 group.add_option(
Stefan Zager1306bd02017-06-23 02:26:465337 '--json', help=('Path of JSON output file to write try job results to,'
5338 'or "-" for stdout.'))
[email protected]b015fac2016-02-26 14:52:015339 parser.add_option_group(group)
5340 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 22:15:015341 _add_codereview_issue_select_options(parser)
[email protected]b015fac2016-02-26 14:52:015342 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 22:15:015343 _process_codereview_issue_select_options(parser, options)
[email protected]b015fac2016-02-26 14:52:015344 if args:
5345 parser.error('Unrecognized args: %s' % ' '.join(args))
5346
5347 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 22:15:015348 cl = Changelist(
5349 issue=options.issue, codereview=options.forced_codereview,
5350 auth_config=auth_config)
[email protected]b015fac2016-02-26 14:52:015351 if not cl.GetIssue():
5352 parser.error('Need to upload first')
5353
tandrii221ab252016-10-06 15:12:045354 patchset = options.patchset
5355 if not patchset:
5356 patchset = cl.GetMostRecentPatchset()
5357 if not patchset:
5358 parser.error('Codereview doesn\'t know about issue %s. '
5359 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 15:27:485360 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 15:12:045361 cl.GetIssue())
5362
[email protected]b015fac2016-02-26 14:52:015363 try:
tandrii221ab252016-10-06 15:12:045364 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
[email protected]b015fac2016-02-26 14:52:015365 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 16:17:495366 print('Buildbucket error: %s' % ex)
[email protected]b015fac2016-02-26 14:52:015367 return 1
qyearsley53f48a12016-09-01 17:45:135368 if options.json:
5369 write_try_results_json(options.json, jobs)
5370 else:
5371 print_try_jobs(options, jobs)
[email protected]b015fac2016-02-26 14:52:015372 return 0
5373
5374
[email protected]0633fb42013-08-16 20:06:145375@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:025376@metrics.collector.collect_metrics('git cl upstream')
[email protected]cc51cd02010-12-23 00:48:395377def CMDupstream(parser, args):
[email protected]d9c1b202013-07-24 23:52:115378 """Prints or sets the name of the upstream branch, if any."""
[email protected]97ae58e2011-03-18 00:29:205379 _, args = parser.parse_args(args)
[email protected]ac0ba332012-08-09 23:42:535380 if len(args) > 1:
[email protected]27bb3872011-05-30 20:33:195381 parser.error('Unrecognized args: %s' % ' '.join(args))
[email protected]ac0ba332012-08-09 23:42:535382
[email protected]cc51cd02010-12-23 00:48:395383 cl = Changelist()
[email protected]ac0ba332012-08-09 23:42:535384 if args:
5385 # One arg means set upstream branch.
[email protected]c9cf90a2014-04-28 20:32:315386 branch = cl.GetBranch()
stip7a3dd352016-09-23 00:32:285387 RunGit(['branch', '--set-upstream-to', args[0], branch])
[email protected]ac0ba332012-08-09 23:42:535388 cl = Changelist()
vapiera7fbd5a2016-06-16 16:17:495389 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
[email protected]c9cf90a2014-04-28 20:32:315390
5391 # Clear configured merge-base, if there is one.
5392 git_common.remove_merge_base(branch)
[email protected]ac0ba332012-08-09 23:42:535393 else:
vapiera7fbd5a2016-06-16 16:17:495394 print(cl.GetUpstreamBranch())
[email protected]cc51cd02010-12-23 00:48:395395 return 0
5396
5397
Edward Lemur5ba1e9c2018-07-23 18:19:025398@metrics.collector.collect_metrics('git cl web')
[email protected]00858c82013-12-02 23:08:035399def CMDweb(parser, args):
5400 """Opens the current CL in the web browser."""
5401 _, args = parser.parse_args(args)
5402 if args:
5403 parser.error('Unrecognized args: %s' % ' '.join(args))
5404
5405 issue_url = Changelist().GetIssueURL()
5406 if not issue_url:
vapiera7fbd5a2016-06-16 16:17:495407 print('ERROR No issue to open', file=sys.stderr)
[email protected]00858c82013-12-02 23:08:035408 return 1
5409
5410 webbrowser.open(issue_url)
5411 return 0
5412
5413
Edward Lemur5ba1e9c2018-07-23 18:19:025414@metrics.collector.collect_metrics('git cl set-commit')
[email protected]27bb3872011-05-30 20:33:195415def CMDset_commit(parser, args):
[email protected]d9c1b202013-07-24 23:52:115416 """Sets the commit bit to trigger the Commit Queue."""
[email protected]fa330e82016-04-13 17:09:525417 parser.add_option('-d', '--dry-run', action='store_true',
5418 help='trigger in dry run mode')
5419 parser.add_option('-c', '--clear', action='store_true',
5420 help='stop CQ run, if any')
[email protected]cf6a5d22015-04-09 22:02:005421 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 21:40:405422 _add_codereview_issue_select_options(parser)
[email protected]cf6a5d22015-04-09 22:02:005423 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 21:40:405424 _process_codereview_issue_select_options(parser, options)
[email protected]cf6a5d22015-04-09 22:02:005425 auth_config = auth.extract_auth_config_from_options(options)
[email protected]27bb3872011-05-30 20:33:195426 if args:
5427 parser.error('Unrecognized args: %s' % ' '.join(args))
[email protected]fa330e82016-04-13 17:09:525428 if options.dry_run and options.clear:
5429 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5430
iannuccie53c9352016-08-17 21:40:405431 cl = Changelist(auth_config=auth_config, issue=options.issue,
5432 codereview=options.forced_codereview)
[email protected]fa330e82016-04-13 17:09:525433 if options.clear:
tandriid9e5ce52016-07-13 09:32:595434 state = _CQState.NONE
[email protected]fa330e82016-04-13 17:09:525435 elif options.dry_run:
5436 state = _CQState.DRY_RUN
5437 else:
5438 state = _CQState.COMMIT
5439 if not cl.GetIssue():
5440 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 10:01:595441 cl.SetCQState(state)
[email protected]27bb3872011-05-30 20:33:195442 return 0
5443
5444
Edward Lemur5ba1e9c2018-07-23 18:19:025445@metrics.collector.collect_metrics('git cl set-close')
[email protected]411034a2013-02-26 15:12:015446def CMDset_close(parser, args):
[email protected]d9c1b202013-07-24 23:52:115447 """Closes the issue."""
iannuccie53c9352016-08-17 21:40:405448 _add_codereview_issue_select_options(parser)
[email protected]cf6a5d22015-04-09 22:02:005449 auth.add_auth_options(parser)
5450 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 21:40:405451 _process_codereview_issue_select_options(parser, options)
[email protected]cf6a5d22015-04-09 22:02:005452 auth_config = auth.extract_auth_config_from_options(options)
[email protected]411034a2013-02-26 15:12:015453 if args:
5454 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 21:40:405455 cl = Changelist(auth_config=auth_config, issue=options.issue,
5456 codereview=options.forced_codereview)
[email protected]411034a2013-02-26 15:12:015457 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-06 00:53:095458 if not cl.GetIssue():
5459 DieWithError('ERROR No issue to close')
[email protected]411034a2013-02-26 15:12:015460 cl.CloseIssue()
5461 return 0
5462
5463
Edward Lemur5ba1e9c2018-07-23 18:19:025464@metrics.collector.collect_metrics('git cl diff')
[email protected]87b9bf02013-09-26 20:35:155465def CMDdiff(parser, args):
[email protected]37b2ec02015-04-03 00:49:155466 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 21:03:205467 parser.add_option(
5468 '--stat',
5469 action='store_true',
5470 dest='stat',
5471 help='Generate a diffstat')
[email protected]cf6a5d22015-04-09 22:02:005472 auth.add_auth_options(parser)
5473 options, args = parser.parse_args(args)
5474 auth_config = auth.extract_auth_config_from_options(options)
5475 if args:
5476 parser.error('Unrecognized args: %s' % ' '.join(args))
[email protected]46309bf2015-04-03 21:04:495477
[email protected]cf6a5d22015-04-09 22:02:005478 cl = Changelist(auth_config=auth_config)
[email protected]78dc9842013-11-25 18:43:445479 issue = cl.GetIssue()
[email protected]87b9bf02013-09-26 20:35:155480 branch = cl.GetBranch()
[email protected]78dc9842013-11-25 18:43:445481 if not issue:
5482 DieWithError('No issue found for current branch (%s)' % branch)
[email protected]87b9bf02013-09-26 20:35:155483
Aaron Gablea718c3e2017-08-29 00:47:285484 base = cl._GitGetBranchConfigValue('last-upload-hash')
5485 if not base:
5486 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5487 if not base:
5488 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5489 revision_info = detail['revisions'][detail['current_revision']]
5490 fetch_info = revision_info['fetch']['http']
5491 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5492 base = 'FETCH_HEAD'
[email protected]87b9bf02013-09-26 20:35:155493
Aaron Gablea718c3e2017-08-29 00:47:285494 cmd = ['git', 'diff']
5495 if options.stat:
5496 cmd.append('--stat')
5497 cmd.append(base)
5498 subprocess2.check_call(cmd)
[email protected]87b9bf02013-09-26 20:35:155499
5500 return 0
5501
5502
Edward Lemur5ba1e9c2018-07-23 18:19:025503@metrics.collector.collect_metrics('git cl owners')
[email protected]faf3fdf2013-09-20 02:11:485504def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 22:08:005505 """Finds potential owners for reviewing."""
[email protected]faf3fdf2013-09-20 02:11:485506 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:565507 '--ignore-current',
5508 action='store_true',
5509 help='Ignore the CL\'s current reviewers and start from scratch.')
5510 parser.add_option(
[email protected]faf3fdf2013-09-20 02:11:485511 '--no-color',
5512 action='store_true',
5513 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 22:08:005514 parser.add_option(
5515 '--batch',
5516 action='store_true',
5517 help='Do not run interactively, just suggest some')
[email protected]cf6a5d22015-04-09 22:02:005518 auth.add_auth_options(parser)
[email protected]faf3fdf2013-09-20 02:11:485519 options, args = parser.parse_args(args)
[email protected]cf6a5d22015-04-09 22:02:005520 auth_config = auth.extract_auth_config_from_options(options)
[email protected]faf3fdf2013-09-20 02:11:485521
5522 author = RunGit(['config', 'user.email']).strip() or None
5523
[email protected]cf6a5d22015-04-09 22:02:005524 cl = Changelist(auth_config=auth_config)
[email protected]faf3fdf2013-09-20 02:11:485525
5526 if args:
5527 if len(args) > 1:
5528 parser.error('Unknown args')
5529 base_branch = args[0]
5530 else:
5531 # Default to diffing against the common ancestor of the upstream branch.
[email protected]8b0553c2014-02-11 00:33:375532 base_branch = cl.GetCommonAncestorWithUpstream()
[email protected]faf3fdf2013-09-20 02:11:485533
5534 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 22:08:005535 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5536
5537 if options.batch:
5538 db = owners.Database(change.RepositoryRoot(), file, os.path)
5539 print('\n'.join(db.reviewers_for(affected_files, author)))
5540 return 0
5541
[email protected]faf3fdf2013-09-20 02:11:485542 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 22:08:005543 affected_files,
Jochen Eisinger72606f82017-04-04 08:44:185544 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-06 23:50:145545 author,
Sidney San Martín8e6f58c2018-06-08 01:02:565546 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-06 23:50:145547 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 08:55:065548 disable_color=options.no_color,
5549 override_files=change.OriginalOwnersFiles()).run()
[email protected]faf3fdf2013-09-20 02:11:485550
5551
Aiden Bennerc08566e2018-10-03 17:52:425552def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
[email protected]e0a7c5d2015-02-23 20:30:085553 """Generates a diff command."""
5554 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:425555 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5556
5557 if not allow_prefix:
5558 diff_cmd += ['--no-prefix']
5559
5560 diff_cmd += [diff_type, upstream_commit, '--']
[email protected]e0a7c5d2015-02-23 20:30:085561
5562 if args:
5563 for arg in args:
[email protected]6f7fa5e2016-01-20 19:32:215564 if os.path.isdir(arg) or os.path.isfile(arg):
[email protected]e0a7c5d2015-02-23 20:30:085565 diff_cmd.append(arg)
5566 else:
5567 DieWithError('Argument "%s" is not a file or a directory' % arg)
[email protected]e0a7c5d2015-02-23 20:30:085568
5569 return diff_cmd
5570
Andrii Shyshkalov18975322017-01-25 15:44:135571
[email protected]6f7fa5e2016-01-20 19:32:215572def MatchingFileType(file_name, extensions):
5573 """Returns true if the file name ends with one of the given extensions."""
5574 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
[email protected]e0a7c5d2015-02-23 20:30:085575
Andrii Shyshkalov18975322017-01-25 15:44:135576
[email protected]555cfe42014-01-29 18:21:395577@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:025578@metrics.collector.collect_metrics('git cl format')
[email protected]fab8f822013-05-06 17:43:095579def CMDformat(parser, args):
[email protected]9d0644d2015-06-05 23:16:545580 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 00:19:145581 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 13:07:515582 GN_EXTS = ['.gn', '.gni', '.typemap']
[email protected]3b7e15c2014-01-21 17:44:475583 parser.add_option('--full', action='store_true',
5584 help='Reformat the full content of all touched files')
5585 parser.add_option('--dry-run', action='store_true',
5586 help='Don\'t modify any file on disk.')
[email protected]9d0644d2015-06-05 23:16:545587 parser.add_option('--python', action='store_true',
5588 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 00:19:145589 parser.add_option('--js', action='store_true',
5590 help='Format javascript code with clang-format.')
[email protected]04d5a222014-03-07 18:30:425591 parser.add_option('--diff', action='store_true',
5592 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-16 00:51:045593 parser.add_option('--presubmit', action='store_true',
5594 help='Used when running the script from a presubmit.')
[email protected]fab8f822013-05-06 17:43:095595 opts, args = parser.parse_args(args)
[email protected]fab8f822013-05-06 17:43:095596
Daniel Chengc55eecf2016-12-30 11:11:025597 # Normalize any remaining args against the current path, so paths relative to
5598 # the current directory are still resolved as expected.
5599 args = [os.path.join(os.getcwd(), arg) for arg in args]
5600
[email protected]ff7a1fb2013-12-10 19:21:415601 # git diff generates paths against the root of the repository. Change
5602 # to that directory so clang-format can find files even within subdirs.
[email protected]8b0553c2014-02-11 00:33:375603 rel_base_path = settings.GetRelativeRoot()
[email protected]ff7a1fb2013-12-10 19:21:415604 if rel_base_path:
5605 os.chdir(rel_base_path)
5606
[email protected]29e47272013-05-17 17:01:465607 # Grab the merge-base commit, i.e. the upstream commit of the current
5608 # branch when it was created or the last time it was rebased. This is
5609 # to cover the case where the user may have called "git fetch origin",
5610 # moving the origin branch to a newer commit, but hasn't rebased yet.
5611 upstream_commit = None
5612 cl = Changelist()
5613 upstream_branch = cl.GetUpstreamBranch()
5614 if upstream_branch:
5615 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5616 upstream_commit = upstream_commit.strip()
5617
5618 if not upstream_commit:
5619 DieWithError('Could not find base commit for this branch. '
5620 'Are you in detached state?')
5621
[email protected]6f7fa5e2016-01-20 19:32:215622 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5623 diff_output = RunGit(changed_files_cmd)
5624 diff_files = diff_output.splitlines()
[email protected]ad21b922016-01-28 17:48:425625 # Filter out files deleted by this CL
5626 diff_files = [x for x in diff_files if os.path.isfile(x)]
[email protected]e0a7c5d2015-02-23 20:30:085627
Christopher Lamc5ba6922017-01-24 00:19:145628 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:545629 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 00:19:145630
[email protected]6f7fa5e2016-01-20 19:32:215631 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5632 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5633 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
[email protected]8b61f112016-02-05 13:28:585634 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
[email protected]29e47272013-05-17 17:01:465635
[email protected]3ac1c4e2014-01-16 02:44:425636 top_dir = os.path.normpath(
5637 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5638
[email protected]e0a7c5d2015-02-23 20:30:085639 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5640 # formatted. This is used to block during the presubmit.
5641 return_value = 0
5642
[email protected]0b35f5d2016-02-25 22:39:235643 if clang_diff_files:
[email protected]5573df12016-04-12 18:34:105644 # Locate the clang-format binary in the checkout
5645 try:
5646 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 15:33:575647 except clang_format.NotFoundError as e:
[email protected]5573df12016-04-12 18:34:105648 DieWithError(e)
5649
[email protected]0b35f5d2016-02-25 22:39:235650 if opts.full:
[email protected]e0a7c5d2015-02-23 20:30:085651 cmd = [clang_format_tool]
5652 if not opts.dry_run and not opts.diff:
5653 cmd.append('-i')
[email protected]6f7fa5e2016-01-20 19:32:215654 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
[email protected]e0a7c5d2015-02-23 20:30:085655 if opts.diff:
5656 sys.stdout.write(stdout)
[email protected]0b35f5d2016-02-25 22:39:235657 else:
5658 env = os.environ.copy()
5659 env['PATH'] = str(os.path.dirname(clang_format_tool))
5660 try:
5661 script = clang_format.FindClangFormatScriptInChromiumTree(
5662 'clang-format-diff.py')
vapierfd77ac72016-06-16 15:33:575663 except clang_format.NotFoundError as e:
[email protected]0b35f5d2016-02-25 22:39:235664 DieWithError(e)
[email protected]d6ddc1c2013-10-25 15:36:325665
[email protected]0b35f5d2016-02-25 22:39:235666 cmd = [sys.executable, script, '-p0']
5667 if not opts.dry_run and not opts.diff:
5668 cmd.append('-i')
[email protected]d6ddc1c2013-10-25 15:36:325669
[email protected]0b35f5d2016-02-25 22:39:235670 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5671 diff_output = RunGit(diff_cmd)
[email protected]6f7fa5e2016-01-20 19:32:215672
[email protected]0b35f5d2016-02-25 22:39:235673 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5674 if opts.diff:
5675 sys.stdout.write(stdout)
5676 if opts.dry_run and len(stdout) > 0:
5677 return_value = 2
[email protected]fab8f822013-05-06 17:43:095678
[email protected]9d0644d2015-06-05 23:16:545679 # Similar code to above, but using yapf on .py files rather than clang-format
5680 # on C/C++ files
Aiden Bennerc08566e2018-10-03 17:52:425681 if opts.python and python_diff_files:
[email protected]9d0644d2015-06-05 23:16:545682 yapf_tool = gclient_utils.FindExecutable('yapf')
5683 if yapf_tool is None:
5684 DieWithError('yapf not found in PATH')
5685
Aiden Bennerc08566e2018-10-03 17:52:425686 # If we couldn't find a yapf file we'll default to the chromium style
5687 # specified in depot_tools.
5688 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5689 chromium_default_yapf_style = os.path.join(depot_tools_path,
5690 YAPF_CONFIG_FILENAME)
5691
5692 # Note: yapf still seems to fix indentation of the entire file
5693 # even if line ranges are specified.
5694 # See https://github.com/google/yapf/issues/499
5695 if not opts.full:
5696 py_line_diffs = _ComputeDiffLineRanges(python_diff_files, upstream_commit)
5697
5698 # Used for caching.
5699 yapf_configs = {}
5700 for f in python_diff_files:
5701 # Find the yapf style config for the current file, defaults to depot
5702 # tools default.
5703 yapf_config = _FindYapfConfigFile(
5704 os.path.abspath(f), yapf_configs, top_dir,
5705 chromium_default_yapf_style)
5706
5707 cmd = [yapf_tool, '--style', yapf_config, f]
5708
5709 has_formattable_lines = False
5710 if not opts.full:
5711 # Only run yapf over changed line ranges.
5712 for diff_start, diff_len in py_line_diffs[f]:
5713 diff_end = diff_start + diff_len - 1
5714 # Yapf errors out if diff_end < diff_start but this
5715 # is a valid line range diff for a removal.
5716 if diff_end >= diff_start:
5717 has_formattable_lines = True
5718 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5719 # If all line diffs were removals we have nothing to format.
5720 if not has_formattable_lines:
5721 continue
5722
5723 if opts.diff or opts.dry_run:
5724 cmd += ['--diff']
5725 # Will return non-zero exit code if non-empty diff.
5726 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5727 if opts.diff:
5728 sys.stdout.write(stdout)
5729 elif len(stdout) > 0:
5730 return_value = 2
5731 else:
5732 cmd += ['-i']
5733 RunCommand(cmd, cwd=top_dir)
[email protected]9d0644d2015-06-05 23:16:545734
[email protected]6f7fa5e2016-01-20 19:32:215735 # Dart's formatter does not have the nice property of only operating on
5736 # modified chunks, so hard code full.
5737 if dart_diff_files:
[email protected]e0a7c5d2015-02-23 20:30:085738 try:
5739 command = [dart_format.FindDartFmtToolInChromiumTree()]
5740 if not opts.dry_run and not opts.diff:
5741 command.append('-w')
[email protected]6f7fa5e2016-01-20 19:32:215742 command.extend(dart_diff_files)
[email protected]e0a7c5d2015-02-23 20:30:085743
[email protected]6593d932016-03-03 15:41:155744 stdout = RunCommand(command, cwd=top_dir)
[email protected]e0a7c5d2015-02-23 20:30:085745 if opts.dry_run and stdout:
5746 return_value = 2
5747 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 16:17:495748 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5749 'found in this checkout. Files in other languages are still '
5750 'formatted.')
[email protected]e0a7c5d2015-02-23 20:30:085751
[email protected]8b61f112016-02-05 13:28:585752 # Format GN build files. Always run on full build files for canonical form.
5753 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 15:44:135754 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 23:19:125755 if opts.dry_run or opts.diff:
5756 cmd.append('--dry-run')
[email protected]8b61f112016-02-05 13:28:585757 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 23:19:125758 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5759 shell=sys.platform == 'win32',
5760 cwd=top_dir)
5761 if opts.dry_run and gn_ret == 2:
5762 return_value = 2 # Not formatted.
5763 elif opts.diff and gn_ret == 2:
5764 # TODO this should compute and print the actual diff.
5765 print("This change has GN build file diff for " + gn_diff_file)
5766 elif gn_ret != 0:
5767 # For non-dry run cases (and non-2 return values for dry-run), a
5768 # nonzero error code indicates a failure, probably because the file
5769 # doesn't parse.
5770 DieWithError("gn format failed on " + gn_diff_file +
5771 "\nTry running 'gn format' on this file manually.")
[email protected]8b61f112016-02-05 13:28:585772
Ilya Shermane081cbe2017-08-16 00:51:045773 # Skip the metrics formatting from the global presubmit hook. These files have
5774 # a separate presubmit hook that issues an error if the files need formatting,
5775 # whereas the top-level presubmit script merely issues a warning. Formatting
5776 # these files is somewhat slow, so it's important not to duplicate the work.
5777 if not opts.presubmit:
5778 for xml_dir in GetDirtyMetricsDirs(diff_files):
5779 tool_dir = os.path.join(top_dir, xml_dir)
5780 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5781 if opts.dry_run or opts.diff:
5782 cmd.append('--diff')
Ilya Sherman235b70d2017-08-23 00:46:385783 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-16 00:51:045784 if opts.diff:
5785 sys.stdout.write(stdout)
5786 if opts.dry_run and stdout:
5787 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 16:08:505788
[email protected]e0a7c5d2015-02-23 20:30:085789 return return_value
[email protected]fab8f822013-05-06 17:43:095790
Steven Holte2e664bf2017-04-21 20:10:475791def GetDirtyMetricsDirs(diff_files):
5792 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5793 metrics_xml_dirs = [
5794 os.path.join('tools', 'metrics', 'actions'),
5795 os.path.join('tools', 'metrics', 'histograms'),
5796 os.path.join('tools', 'metrics', 'rappor'),
5797 os.path.join('tools', 'metrics', 'ukm')]
5798 for xml_dir in metrics_xml_dirs:
5799 if any(file.startswith(xml_dir) for file in xml_diff_files):
5800 yield xml_dir
5801
[email protected]fab8f822013-05-06 17:43:095802
[email protected]84a80c42015-09-22 20:40:375803@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:025804@metrics.collector.collect_metrics('git cl checkout')
[email protected]84a80c42015-09-22 20:40:375805def CMDcheckout(parser, args):
[email protected]5df290f2016-04-11 16:12:295806 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
[email protected]84a80c42015-09-22 20:40:375807 _, args = parser.parse_args(args)
5808
5809 if len(args) != 1:
5810 parser.print_help()
5811 return 1
5812
[email protected]f86c7d32016-04-01 19:27:305813 issue_arg = ParseIssueNumberArgument(args[0])
[email protected]de6c9a12016-04-11 15:33:535814 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 13:45:095815 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 11:35:215816
[email protected]abd27e52016-04-11 15:43:325817 target_issue = str(issue_arg.issue)
[email protected]84a80c42015-09-22 20:40:375818
[email protected]5df290f2016-04-11 16:12:295819 def find_issues(issueprefix):
[email protected]26c8fd22016-04-11 21:33:215820 output = RunGit(['config', '--local', '--get-regexp',
5821 r'branch\..*\.%s' % issueprefix],
5822 error_ok=True)
5823 for key, issue in [x.split() for x in output.splitlines()]:
[email protected]5df290f2016-04-11 16:12:295824 if issue == target_issue:
5825 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
[email protected]84a80c42015-09-22 20:40:375826
[email protected]5df290f2016-04-11 16:12:295827 branches = []
5828 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 23:19:375829 branches.extend(find_issues(cls.IssueConfigKey()))
[email protected]84a80c42015-09-22 20:40:375830 if len(branches) == 0:
vapiera7fbd5a2016-06-16 16:17:495831 print('No branch found for issue %s.' % target_issue)
[email protected]84a80c42015-09-22 20:40:375832 return 1
5833 if len(branches) == 1:
5834 RunGit(['checkout', branches[0]])
5835 else:
vapiera7fbd5a2016-06-16 16:17:495836 print('Multiple branches match issue %s:' % target_issue)
[email protected]84a80c42015-09-22 20:40:375837 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 16:17:495838 print('%d: %s' % (i, branches[i]))
[email protected]84a80c42015-09-22 20:40:375839 which = raw_input('Choose by index: ')
5840 try:
5841 RunGit(['checkout', branches[int(which)]])
5842 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 16:17:495843 print('Invalid selection, not checking out any branch.')
[email protected]84a80c42015-09-22 20:40:375844 return 1
5845
5846 return 0
5847
5848
[email protected]29404b52014-09-08 22:58:005849def CMDlol(parser, args):
5850 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 16:17:495851 print(zlib.decompress(base64.b64decode(
[email protected]3421c992014-11-02 02:20:325852 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5853 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5854 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 16:17:495855 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
[email protected]29404b52014-09-08 22:58:005856 return 0
5857
5858
[email protected]d9c1b202013-07-24 23:52:115859class OptionParser(optparse.OptionParser):
5860 """Creates the option parse and add --verbose support."""
5861 def __init__(self, *args, **kwargs):
[email protected]0633fb42013-08-16 20:06:145862 optparse.OptionParser.__init__(
5863 self, *args, prog='git cl', version=__version__, **kwargs)
[email protected]d9c1b202013-07-24 23:52:115864 self.add_option(
5865 '-v', '--verbose', action='count', default=0,
5866 help='Use 2 times for more debugging info')
5867
Edward Lemur5ba1e9c2018-07-23 18:19:025868 def parse_args(self, args=None, _values=None):
5869 # Create an optparse.Values object that will store only the actual passed
5870 # options, without the defaults.
5871 actual_options = optparse.Values()
5872 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5873 # Create an optparse.Values object with the default options.
5874 options = optparse.Values(self.get_default_values().__dict__)
5875 # Update it with the options passed by the user.
5876 options._update_careful(actual_options.__dict__)
5877 # Store the options passed by the user in an _actual_options attribute.
5878 # We store only the keys, and not the values, since the values can contain
5879 # arbitrary information, which might be PII.
5880 metrics.collector.add('arguments', actual_options.__dict__.keys())
5881
[email protected]d9c1b202013-07-24 23:52:115882 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 16:44:415883 logging.basicConfig(
5884 level=levels[min(options.verbose, len(levels) - 1)],
5885 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5886 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:215887
[email protected]d9c1b202013-07-24 23:52:115888 return options, args
5889
[email protected]d9c1b202013-07-24 23:52:115890
[email protected]cc51cd02010-12-23 00:48:395891def main(argv):
[email protected]82798cb2012-02-23 18:16:125892 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 16:17:495893 print('\nYour python version %s is unsupported, please upgrade.\n' %
5894 (sys.version.split(' ', 1)[0],), file=sys.stderr)
[email protected]82798cb2012-02-23 18:16:125895 return 2
[email protected]2e23ce32013-05-07 12:42:285896
[email protected]ddd59412011-11-30 14:20:385897 # Reload settings.
5898 global settings
5899 settings = Settings()
5900
Edward Lemurad463c92018-07-25 21:31:235901 if not metrics.DISABLE_METRICS_COLLECTION:
5902 metrics.collector.add('project_urls', [settings.GetViewVCUrl().strip('/+')])
[email protected]39c0b222013-08-17 16:57:015903 colorize_CMDstatus_doc()
[email protected]0633fb42013-08-16 20:06:145904 dispatcher = subcommand.CommandDispatcher(__name__)
5905 try:
5906 return dispatcher.execute(OptionParser(), argv)
[email protected]eed4df32015-04-10 21:30:205907 except auth.AuthenticationError as e:
5908 DieWithError(str(e))
vapierfd77ac72016-06-16 15:33:575909 except urllib2.HTTPError as e:
[email protected]0633fb42013-08-16 20:06:145910 if e.code != 500:
5911 raise
5912 DieWithError(
5913 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5914 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
[email protected]013731e2015-02-26 18:28:435915 return 0
[email protected]cc51cd02010-12-23 00:48:395916
5917
5918if __name__ == '__main__':
[email protected]2e23ce32013-05-07 12:42:285919 # These affect sys.stdout so do it outside of main() to simplify mocks in
5920 # unit testing.
[email protected]6f09cd92011-04-01 16:38:125921 fix_encoding.fix_encoding()
[email protected]596cd5c2016-04-04 21:34:395922 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:575923 with metrics.collector.print_notice_and_exit():
[email protected]013731e2015-02-26 18:28:435924 sys.exit(main(sys.argv[1:]))