blob: 2439bcfdec7d298a190bd40d06207b2df2aa2cf5 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000034import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000035import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000036import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000037import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038
39try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080040 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041except ImportError:
42 pass
43
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000044from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000045from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000058import metrics
piman@chromium.org336f9122014-09-04 02:16:55 +000059import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000060import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000062import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040064import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067import watchlists
68
tandrii7400cf02016-06-21 08:48:07 -070069__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000070
tandrii9d2c7a32016-06-22 03:42:45 -070071COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070072DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080073POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000075REFS_THAT_ALIAS_TO_OTHER_REFS = {
76 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
77 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
78}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079
thestig@chromium.org44202a22014-03-11 19:22:18 +000080# Valid extensions for files we want to lint.
81DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
82DEFAULT_LINT_IGNORE_REGEX = r"$^"
83
Aiden Bennerc08566e2018-10-03 17:52:42 +000084# File name for yapf style config files.
85YAPF_CONFIG_FILENAME = '.style.yapf'
86
borenet6c0efe62016-10-19 08:13:29 -070087# Buildbucket master name prefix.
88MASTER_PREFIX = 'master.'
89
Edward Lemur83bd7f42018-10-10 00:14:21 +000090# TODO(crbug.com/881860): Remove
91# Log gerrit failures to a gerrit_util.GERRIT_ERR_LOG_FILE.
92GERRIT_ERR_LOGGER = logging.getLogger('GerritErrorLogs')
93
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000094# Shortcut since it quickly becomes redundant.
95Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000096
maruel@chromium.orgddd59412011-11-30 14:20:38 +000097# Initialized in main()
98settings = None
99
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100100# Used by tests/git_cl_test.py to add extra logging.
101# Inside the weirdly failing test, add this:
102# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700103# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100104_IS_BEING_TESTED = False
105
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000106
Christopher Lamf732cd52017-01-24 12:40:11 +1100107def DieWithError(message, change_desc=None):
108 if change_desc:
109 SaveDescriptionBackup(change_desc)
110
vapiera7fbd5a2016-06-16 09:17:49 -0700111 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112 sys.exit(1)
113
114
Christopher Lamf732cd52017-01-24 12:40:11 +1100115def SaveDescriptionBackup(change_desc):
116 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000117 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100118 backup_file = open(backup_path, 'w')
119 backup_file.write(change_desc.description)
120 backup_file.close()
121
122
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123def GetNoGitPagerEnv():
124 env = os.environ.copy()
125 # 'cat' is a magical git string that disables pagers on all platforms.
126 env['GIT_PAGER'] = 'cat'
127 return env
128
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000129
bsep@chromium.org627d9002016-04-29 00:00:52 +0000130def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000132 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000133 except subprocess2.CalledProcessError as e:
134 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000135 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000136 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000137 'Command "%s" failed.\n%s' % (
138 ' '.join(args), error_message or e.stdout or ''))
139 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000140
141
142def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000143 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000144 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000145
146
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000147def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000148 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700149 if suppress_stderr:
150 stderr = subprocess2.VOID
151 else:
152 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000153 try:
tandrii5d48c322016-08-18 16:19:37 -0700154 (out, _), code = subprocess2.communicate(['git'] + args,
155 env=GetNoGitPagerEnv(),
156 stdout=subprocess2.PIPE,
157 stderr=stderr)
158 return code, out
159 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900160 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700161 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000162
163
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000164def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000165 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000166 return RunGitWithCode(args, suppress_stderr=True)[1]
167
168
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000169def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000170 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000171 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000172 return (version.startswith(prefix) and
173 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000174
175
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000176def BranchExists(branch):
177 """Return True if specified branch exists."""
178 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
179 suppress_stderr=True)
180 return not code
181
182
tandrii2a16b952016-10-19 07:09:44 -0700183def time_sleep(seconds):
184 # Use this so that it can be mocked in tests without interfering with python
185 # system machinery.
186 import time # Local import to discourage others from importing time globally.
187 return time.sleep(seconds)
188
189
maruel@chromium.org90541732011-04-01 17:54:18 +0000190def ask_for_data(prompt):
191 try:
192 return raw_input(prompt)
193 except KeyboardInterrupt:
194 # Hide the exception.
195 sys.exit(1)
196
197
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100198def confirm_or_exit(prefix='', action='confirm'):
199 """Asks user to press enter to continue or press Ctrl+C to abort."""
200 if not prefix or prefix.endswith('\n'):
201 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100202 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100203 mid = ' Press'
204 elif prefix.endswith(' '):
205 mid = 'press'
206 else:
207 mid = ' press'
208 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
209
210
211def ask_for_explicit_yes(prompt):
212 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
213 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
214 while True:
215 if 'yes'.startswith(result):
216 return True
217 if 'no'.startswith(result):
218 return False
219 result = ask_for_data('Please, type yes or no: ').lower()
220
221
tandrii5d48c322016-08-18 16:19:37 -0700222def _git_branch_config_key(branch, key):
223 """Helper method to return Git config key for a branch."""
224 assert branch, 'branch name is required to set git config for it'
225 return 'branch.%s.%s' % (branch, key)
226
227
228def _git_get_branch_config_value(key, default=None, value_type=str,
229 branch=False):
230 """Returns git config value of given or current branch if any.
231
232 Returns default in all other cases.
233 """
234 assert value_type in (int, str, bool)
235 if branch is False: # Distinguishing default arg value from None.
236 branch = GetCurrentBranch()
237
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000238 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700239 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000240
tandrii5d48c322016-08-18 16:19:37 -0700241 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700242 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700243 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700244 # git config also has --int, but apparently git config suffers from integer
245 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700246 args.append(_git_branch_config_key(branch, key))
247 code, out = RunGitWithCode(args)
248 if code == 0:
249 value = out.strip()
250 if value_type == int:
251 return int(value)
252 if value_type == bool:
253 return bool(value.lower() == 'true')
254 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255 return default
256
257
tandrii5d48c322016-08-18 16:19:37 -0700258def _git_set_branch_config_value(key, value, branch=None, **kwargs):
259 """Sets the value or unsets if it's None of a git branch config.
260
261 Valid, though not necessarily existing, branch must be provided,
262 otherwise currently checked out branch is used.
263 """
264 if not branch:
265 branch = GetCurrentBranch()
266 assert branch, 'a branch name OR currently checked out branch is required'
267 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700268 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700269 if value is None:
270 args.append('--unset')
271 elif isinstance(value, bool):
272 args.append('--bool')
273 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700274 else:
tandrii33a46ff2016-08-23 05:53:40 -0700275 # git config also has --int, but apparently git config suffers from integer
276 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700277 value = str(value)
278 args.append(_git_branch_config_key(branch, key))
279 if value is not None:
280 args.append(value)
281 RunGit(args, **kwargs)
282
283
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100284def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700285 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100286
287 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
288 """
289 # Git also stores timezone offset, but it only affects visual display,
290 # actual point in time is defined by this timestamp only.
291 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
292
293
294def _git_amend_head(message, committer_timestamp):
295 """Amends commit with new message and desired committer_timestamp.
296
297 Sets committer timezone to UTC.
298 """
299 env = os.environ.copy()
300 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
301 return RunGit(['commit', '--amend', '-m', message], env=env)
302
303
machenbach@chromium.org45453142015-09-15 08:45:22 +0000304def _get_properties_from_options(options):
305 properties = dict(x.split('=', 1) for x in options.properties)
306 for key, val in properties.iteritems():
307 try:
308 properties[key] = json.loads(val)
309 except ValueError:
310 pass # If a value couldn't be evaluated, treat it as a string.
311 return properties
312
313
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000314def _prefix_master(master):
315 """Convert user-specified master name to full master name.
316
317 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
318 name, while the developers always use shortened master name
319 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
320 function does the conversion for buildbucket migration.
321 """
borenet6c0efe62016-10-19 08:13:29 -0700322 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000323 return master
borenet6c0efe62016-10-19 08:13:29 -0700324 return '%s%s' % (MASTER_PREFIX, master)
325
326
327def _unprefix_master(bucket):
328 """Convert bucket name to shortened master name.
329
330 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
331 name, while the developers always use shortened master name
332 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
333 function does the conversion for buildbucket migration.
334 """
335 if bucket.startswith(MASTER_PREFIX):
336 return bucket[len(MASTER_PREFIX):]
337 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338
339
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000340def _buildbucket_retry(operation_name, http, *args, **kwargs):
341 """Retries requests to buildbucket service and returns parsed json content."""
342 try_count = 0
343 while True:
344 response, content = http.request(*args, **kwargs)
345 try:
346 content_json = json.loads(content)
347 except ValueError:
348 content_json = None
349
350 # Buildbucket could return an error even if status==200.
351 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000352 error = content_json.get('error')
353 if error.get('code') == 403:
354 raise BuildbucketResponseException(
355 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000356 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000357 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 raise BuildbucketResponseException(msg)
359
360 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700361 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 raise BuildbucketResponseException(
363 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700364 'Please file bugs at http://crbug.com, '
365 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366 content)
367 return content_json
368 if response.status < 500 or try_count >= 2:
369 raise httplib2.HttpLib2Error(content)
370
371 # status >= 500 means transient failures.
372 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700373 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374 try_count += 1
375 assert False, 'unreachable'
376
377
qyearsley1fdfcb62016-10-24 13:22:03 -0700378def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700379 """Returns a dict mapping bucket names to builders and tests,
380 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700381 """
qyearsleydd49f942016-10-28 11:57:22 -0700382 # If no bots are listed, we try to get a set of builders and tests based
383 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700384 if not options.bot:
385 change = changelist.GetChange(
386 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700387 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700388 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700389 change=change,
390 changed_files=change.LocalPaths(),
391 repository_root=settings.GetRoot(),
392 default_presubmit=None,
393 project=None,
394 verbose=options.verbose,
395 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700396 if masters is None:
397 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100398 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700399
qyearsley1fdfcb62016-10-24 13:22:03 -0700400 if options.bucket:
401 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700402 if options.master:
403 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700404
qyearsleydd49f942016-10-28 11:57:22 -0700405 # If bots are listed but no master or bucket, then we need to find out
406 # the corresponding master for each bot.
407 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
408 if error_message:
409 option_parser.error(
410 'Tryserver master cannot be found because: %s\n'
411 'Please manually specify the tryserver master, e.g. '
412 '"-m tryserver.chromium.linux".' % error_message)
413 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700414
415
qyearsley123a4682016-10-26 09:12:17 -0700416def _get_bucket_map_for_builders(builders):
417 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700418 map_url = 'https://builders-map.appspot.com/'
419 try:
qyearsley123a4682016-10-26 09:12:17 -0700420 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700421 except urllib2.URLError as e:
422 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
423 (map_url, e))
424 except ValueError as e:
425 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700426 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700427 return None, 'Failed to build master map.'
428
qyearsley123a4682016-10-26 09:12:17 -0700429 bucket_map = {}
430 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800431 bucket = builders_map.get(builder, {}).get('bucket')
432 if bucket:
433 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700434 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700435
436
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800437def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700438 """Sends a request to Buildbucket to trigger try jobs for a changelist.
439
440 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700441 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700442 changelist: Changelist that the try jobs are associated with.
443 buckets: A nested dict mapping bucket names to builders to tests.
444 options: Command-line options.
445 """
tandriide281ae2016-10-12 06:02:30 -0700446 assert changelist.GetIssue(), 'CL must be uploaded first'
447 codereview_url = changelist.GetCodereviewServer()
448 assert codereview_url, 'CL must be uploaded first'
449 patchset = patchset or changelist.GetMostRecentPatchset()
450 assert patchset, 'CL must be uploaded first'
451
452 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700453 # Cache the buildbucket credentials under the codereview host key, so that
454 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700455 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000456 http = authenticator.authorize(httplib2.Http())
457 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700458
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459 buildbucket_put_url = (
460 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000461 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000462 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700463 hostname=codereview_host,
464 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000465 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700466
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700467 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800468 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700469 if options.clobber:
470 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700471 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700472 if extra_properties:
473 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000474
475 batch_req_body = {'builds': []}
476 print_text = []
477 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700478 for bucket, builders_and_tests in sorted(buckets.iteritems()):
479 print_text.append('Bucket: %s' % bucket)
480 master = None
481 if bucket.startswith(MASTER_PREFIX):
482 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000483 for builder, tests in sorted(builders_and_tests.iteritems()):
484 print_text.append(' %s: %s' % (builder, tests))
485 parameters = {
486 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000487 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100488 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000489 'revision': options.revision,
490 }],
tandrii8c5a3532016-11-04 07:52:02 -0700491 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000492 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000493 if 'presubmit' in builder.lower():
494 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000495 if tests:
496 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700497
498 tags = [
499 'builder:%s' % builder,
500 'buildset:%s' % buildset,
501 'user_agent:git_cl_try',
502 ]
503 if master:
504 parameters['properties']['master'] = master
505 tags.append('master:%s' % master)
506
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000507 batch_req_body['builds'].append(
508 {
509 'bucket': bucket,
510 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700512 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000513 }
514 )
515
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000516 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700517 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000518 http,
519 buildbucket_put_url,
520 'PUT',
521 body=json.dumps(batch_req_body),
522 headers={'Content-Type': 'application/json'}
523 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000524 print_text.append('To see results here, run: git cl try-results')
525 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700526 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000527
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000528
tandrii221ab252016-10-06 08:12:04 -0700529def fetch_try_jobs(auth_config, changelist, buildbucket_host,
530 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700531 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000532
qyearsley53f48a12016-09-01 10:45:13 -0700533 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534 """
tandrii221ab252016-10-06 08:12:04 -0700535 assert buildbucket_host
536 assert changelist.GetIssue(), 'CL must be uploaded first'
537 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
538 patchset = patchset or changelist.GetMostRecentPatchset()
539 assert patchset, 'CL must be uploaded first'
540
541 codereview_url = changelist.GetCodereviewServer()
542 codereview_host = urlparse.urlparse(codereview_url).hostname
543 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 if authenticator.has_cached_credentials():
545 http = authenticator.authorize(httplib2.Http())
546 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700547 print('Warning: Some results might be missing because %s' %
548 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700549 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 http = httplib2.Http()
551
552 http.force_exception_to_status_code = True
553
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000554 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700555 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700557 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 params = {'tag': 'buildset:%s' % buildset}
559
560 builds = {}
561 while True:
562 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700563 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700565 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566 for build in content.get('builds', []):
567 builds[build['id']] = build
568 if 'next_cursor' in content:
569 params['start_cursor'] = content['next_cursor']
570 else:
571 break
572 return builds
573
574
qyearsleyeab3c042016-08-24 09:18:28 -0700575def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576 """Prints nicely result of fetch_try_jobs."""
577 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700578 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 return
580
581 # Make a copy, because we'll be modifying builds dictionary.
582 builds = builds.copy()
583 builder_names_cache = {}
584
585 def get_builder(b):
586 try:
587 return builder_names_cache[b['id']]
588 except KeyError:
589 try:
590 parameters = json.loads(b['parameters_json'])
591 name = parameters['builder_name']
592 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700593 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700594 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000595 name = None
596 builder_names_cache[b['id']] = name
597 return name
598
599 def get_bucket(b):
600 bucket = b['bucket']
601 if bucket.startswith('master.'):
602 return bucket[len('master.'):]
603 return bucket
604
605 if options.print_master:
606 name_fmt = '%%-%ds %%-%ds' % (
607 max(len(str(get_bucket(b))) for b in builds.itervalues()),
608 max(len(str(get_builder(b))) for b in builds.itervalues()))
609 def get_name(b):
610 return name_fmt % (get_bucket(b), get_builder(b))
611 else:
612 name_fmt = '%%-%ds' % (
613 max(len(str(get_builder(b))) for b in builds.itervalues()))
614 def get_name(b):
615 return name_fmt % get_builder(b)
616
617 def sort_key(b):
618 return b['status'], b.get('result'), get_name(b), b.get('url')
619
620 def pop(title, f, color=None, **kwargs):
621 """Pop matching builds from `builds` dict and print them."""
622
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000623 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000624 colorize = str
625 else:
626 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
627
628 result = []
629 for b in builds.values():
630 if all(b.get(k) == v for k, v in kwargs.iteritems()):
631 builds.pop(b['id'])
632 result.append(b)
633 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700634 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000635 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700636 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000637
638 total = len(builds)
639 pop(status='COMPLETED', result='SUCCESS',
640 title='Successes:', color=Fore.GREEN,
641 f=lambda b: (get_name(b), b.get('url')))
642 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
643 title='Infra Failures:', color=Fore.MAGENTA,
644 f=lambda b: (get_name(b), b.get('url')))
645 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
646 title='Failures:', color=Fore.RED,
647 f=lambda b: (get_name(b), b.get('url')))
648 pop(status='COMPLETED', result='CANCELED',
649 title='Canceled:', color=Fore.MAGENTA,
650 f=lambda b: (get_name(b),))
651 pop(status='COMPLETED', result='FAILURE',
652 failure_reason='INVALID_BUILD_DEFINITION',
653 title='Wrong master/builder name:', color=Fore.MAGENTA,
654 f=lambda b: (get_name(b),))
655 pop(status='COMPLETED', result='FAILURE',
656 title='Other failures:',
657 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
658 pop(status='COMPLETED',
659 title='Other finished:',
660 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
661 pop(status='STARTED',
662 title='Started:', color=Fore.YELLOW,
663 f=lambda b: (get_name(b), b.get('url')))
664 pop(status='SCHEDULED',
665 title='Scheduled:',
666 f=lambda b: (get_name(b), 'id=%s' % b['id']))
667 # The last section is just in case buildbucket API changes OR there is a bug.
668 pop(title='Other:',
669 f=lambda b: (get_name(b), 'id=%s' % b['id']))
670 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700671 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000672
673
Aiden Bennerc08566e2018-10-03 17:52:42 +0000674def _ComputeDiffLineRanges(files, upstream_commit):
675 """Gets the changed line ranges for each file since upstream_commit.
676
677 Parses a git diff on provided files and returns a dict that maps a file name
678 to an ordered list of range tuples in the form (start_line, count).
679 Ranges are in the same format as a git diff.
680 """
681 # If files is empty then diff_output will be a full diff.
682 if len(files) == 0:
683 return {}
684
685 # Take diff and find the line ranges where there are changes.
686 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
687 diff_output = RunGit(diff_cmd)
688
689 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
690 # 2 capture groups
691 # 0 == fname of diff file
692 # 1 == 'diff_start,diff_count' or 'diff_start'
693 # will match each of
694 # diff --git a/foo.foo b/foo.py
695 # @@ -12,2 +14,3 @@
696 # @@ -12,2 +17 @@
697 # running re.findall on the above string with pattern will give
698 # [('foo.py', ''), ('', '14,3'), ('', '17')]
699
700 curr_file = None
701 line_diffs = {}
702 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
703 if match[0] != '':
704 # Will match the second filename in diff --git a/a.py b/b.py.
705 curr_file = match[0]
706 line_diffs[curr_file] = []
707 else:
708 # Matches +14,3
709 if ',' in match[1]:
710 diff_start, diff_count = match[1].split(',')
711 else:
712 # Single line changes are of the form +12 instead of +12,1.
713 diff_start = match[1]
714 diff_count = 1
715
716 diff_start = int(diff_start)
717 diff_count = int(diff_count)
718
719 # If diff_count == 0 this is a removal we can ignore.
720 line_diffs[curr_file].append((diff_start, diff_count))
721
722 return line_diffs
723
724
725def _FindYapfConfigFile(fpath,
726 yapf_config_cache,
727 top_dir=None,
728 default_style=None):
729 """Checks if a yapf file is in any parent directory of fpath until top_dir.
730
731 Recursively checks parent directories to find yapf file
732 and if no yapf file is found returns default_style.
733 Uses yapf_config_cache as a cache for previously found files.
734 """
735 # Return result if we've already computed it.
736 if fpath in yapf_config_cache:
737 return yapf_config_cache[fpath]
738
739 # Check if there is a style file in the current directory.
740 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
741 dirname = os.path.dirname(fpath)
742 if os.path.isfile(yapf_file):
743 ret = yapf_file
744 elif fpath == top_dir or dirname == fpath:
745 # If we're at the top level directory, or if we're at root
746 # use the chromium default yapf style.
747 ret = default_style
748 else:
749 # Otherwise recurse on the current directory.
750 ret = _FindYapfConfigFile(dirname, yapf_config_cache, top_dir,
751 default_style)
752 yapf_config_cache[fpath] = ret
753 return ret
754
755
qyearsley53f48a12016-09-01 10:45:13 -0700756def write_try_results_json(output_file, builds):
757 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
758
759 The input |builds| dict is assumed to be generated by Buildbucket.
760 Buildbucket documentation: http://goo.gl/G0s101
761 """
762
763 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800764 """Extracts some of the information from one build dict."""
765 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700766 return {
767 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700768 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800769 'builder_name': parameters.get('builder_name'),
770 'created_ts': build.get('created_ts'),
771 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700772 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800773 'result': build.get('result'),
774 'status': build.get('status'),
775 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700776 'url': build.get('url'),
777 }
778
779 converted = []
780 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000781 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700782 write_json(output_file, converted)
783
784
Aaron Gable13101a62018-02-09 13:20:41 -0800785def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000786 """Prints statistics about the change to the user."""
787 # --no-ext-diff is broken in some versions of Git, so try to work around
788 # this by overriding the environment (but there is still a problem if the
789 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000790 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000791 if 'GIT_EXTERNAL_DIFF' in env:
792 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000793
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000794 try:
795 stdout = sys.stdout.fileno()
796 except AttributeError:
797 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000798 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800799 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000800 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000801
802
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000803class BuildbucketResponseException(Exception):
804 pass
805
806
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807class Settings(object):
808 def __init__(self):
809 self.default_server = None
810 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000811 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 self.tree_status_url = None
813 self.viewvc_url = None
814 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000815 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000816 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000817 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000818 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000819 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000820 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821
822 def LazyUpdateIfNeeded(self):
823 """Updates the settings from a codereview.settings file, if available."""
824 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000825 # The only value that actually changes the behavior is
826 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000827 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000828 error_ok=True
829 ).strip().lower()
830
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000831 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000832 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000833 LoadCodereviewSettingsFromFile(cr_settings_file)
834 self.updated = True
835
836 def GetDefaultServerUrl(self, error_ok=False):
837 if not self.default_server:
838 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000839 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000840 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 if error_ok:
842 return self.default_server
843 if not self.default_server:
844 error_message = ('Could not find settings file. You must configure '
845 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000846 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000847 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848 return self.default_server
849
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000850 @staticmethod
851 def GetRelativeRoot():
852 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000853
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000855 if self.root is None:
856 self.root = os.path.abspath(self.GetRelativeRoot())
857 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000859 def GetGitMirror(self, remote='origin'):
860 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000861 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000862 if not os.path.isdir(local_url):
863 return None
864 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
865 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100866 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100867 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000868 if mirror.exists():
869 return mirror
870 return None
871
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 def GetTreeStatusUrl(self, error_ok=False):
873 if not self.tree_status_url:
874 error_message = ('You must configure your tree status URL by running '
875 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000876 self.tree_status_url = self._GetRietveldConfig(
877 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878 return self.tree_status_url
879
880 def GetViewVCUrl(self):
881 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000882 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883 return self.viewvc_url
884
rmistry@google.com90752582014-01-14 21:04:50 +0000885 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000886 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000887
rmistry@google.com78948ed2015-07-08 23:09:57 +0000888 def GetIsSkipDependencyUpload(self, branch_name):
889 """Returns true if specified branch should skip dep uploads."""
890 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
891 error_ok=True)
892
rmistry@google.com5626a922015-02-26 14:03:30 +0000893 def GetRunPostUploadHook(self):
894 run_post_upload_hook = self._GetRietveldConfig(
895 'run-post-upload-hook', error_ok=True)
896 return run_post_upload_hook == "True"
897
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000898 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000899 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000900
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000901 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000902 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000903
ukai@chromium.orge8077812012-02-03 03:41:46 +0000904 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700905 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000906 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700907 self.is_gerrit = (
908 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000909 return self.is_gerrit
910
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000911 def GetSquashGerritUploads(self):
912 """Return true if uploads to Gerrit should be squashed by default."""
913 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700914 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
915 if self.squash_gerrit_uploads is None:
916 # Default is squash now (http://crbug.com/611892#c23).
917 self.squash_gerrit_uploads = not (
918 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
919 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000920 return self.squash_gerrit_uploads
921
tandriia60502f2016-06-20 02:01:53 -0700922 def GetSquashGerritUploadsOverride(self):
923 """Return True or False if codereview.settings should be overridden.
924
925 Returns None if no override has been defined.
926 """
927 # See also http://crbug.com/611892#c23
928 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
929 error_ok=True).strip()
930 if result == 'true':
931 return True
932 if result == 'false':
933 return False
934 return None
935
tandrii@chromium.org28253532016-04-14 13:46:56 +0000936 def GetGerritSkipEnsureAuthenticated(self):
937 """Return True if EnsureAuthenticated should not be done for Gerrit
938 uploads."""
939 if self.gerrit_skip_ensure_authenticated is None:
940 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000941 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000942 error_ok=True).strip() == 'true')
943 return self.gerrit_skip_ensure_authenticated
944
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000945 def GetGitEditor(self):
946 """Return the editor specified in the git config, or None if none is."""
947 if self.git_editor is None:
948 self.git_editor = self._GetConfig('core.editor', error_ok=True)
949 return self.git_editor or None
950
thestig@chromium.org44202a22014-03-11 19:22:18 +0000951 def GetLintRegex(self):
952 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
953 DEFAULT_LINT_REGEX)
954
955 def GetLintIgnoreRegex(self):
956 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
957 DEFAULT_LINT_IGNORE_REGEX)
958
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000959 def GetProject(self):
960 if not self.project:
961 self.project = self._GetRietveldConfig('project', error_ok=True)
962 return self.project
963
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000964 def _GetRietveldConfig(self, param, **kwargs):
965 return self._GetConfig('rietveld.' + param, **kwargs)
966
rmistry@google.com78948ed2015-07-08 23:09:57 +0000967 def _GetBranchConfig(self, branch_name, param, **kwargs):
968 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
969
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970 def _GetConfig(self, param, **kwargs):
971 self.LazyUpdateIfNeeded()
972 return RunGit(['config', param], **kwargs).strip()
973
974
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100975@contextlib.contextmanager
976def _get_gerrit_project_config_file(remote_url):
977 """Context manager to fetch and store Gerrit's project.config from
978 refs/meta/config branch and store it in temp file.
979
980 Provides a temporary filename or None if there was error.
981 """
982 error, _ = RunGitWithCode([
983 'fetch', remote_url,
984 '+refs/meta/config:refs/git_cl/meta/config'])
985 if error:
986 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700987 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100988 (remote_url, error))
989 yield None
990 return
991
992 error, project_config_data = RunGitWithCode(
993 ['show', 'refs/git_cl/meta/config:project.config'])
994 if error:
995 print('WARNING: project.config file not found')
996 yield None
997 return
998
999 with gclient_utils.temporary_directory() as tempdir:
1000 project_config_file = os.path.join(tempdir, 'project.config')
1001 gclient_utils.FileWrite(project_config_file, project_config_data)
1002 yield project_config_file
1003
1004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005def ShortBranchName(branch):
1006 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001007 return branch.replace('refs/heads/', '', 1)
1008
1009
1010def GetCurrentBranchRef():
1011 """Returns branch ref (e.g., refs/heads/master) or None."""
1012 return RunGit(['symbolic-ref', 'HEAD'],
1013 stderr=subprocess2.VOID, error_ok=True).strip() or None
1014
1015
1016def GetCurrentBranch():
1017 """Returns current branch or None.
1018
1019 For refs/heads/* branches, returns just last part. For others, full ref.
1020 """
1021 branchref = GetCurrentBranchRef()
1022 if branchref:
1023 return ShortBranchName(branchref)
1024 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025
1026
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001027class _CQState(object):
1028 """Enum for states of CL with respect to Commit Queue."""
1029 NONE = 'none'
1030 DRY_RUN = 'dry_run'
1031 COMMIT = 'commit'
1032
1033 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1034
1035
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001036class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001037 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001038 self.issue = issue
1039 self.patchset = patchset
1040 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001041 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001042 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001043
1044 @property
1045 def valid(self):
1046 return self.issue is not None
1047
1048
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001049def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001050 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1051 fail_result = _ParsedIssueNumberArgument()
1052
1053 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001054 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001055 if not arg.startswith('http'):
1056 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001057
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001058 url = gclient_utils.UpgradeToHttps(arg)
1059 try:
1060 parsed_url = urlparse.urlparse(url)
1061 except ValueError:
1062 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001063
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001064 if codereview is not None:
1065 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1066 return parsed or fail_result
1067
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001068 results = {}
1069 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1070 parsed = cls.ParseIssueURL(parsed_url)
1071 if parsed is not None:
1072 results[name] = parsed
1073
1074 if not results:
1075 return fail_result
1076 if len(results) == 1:
1077 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001078
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001079 return results['gerrit']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001080
1081
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001082def _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 15:14:38 -08001096class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001097 def __init__(self, issue, url):
1098 self.issue = issue
1099 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001100 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001101
1102 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001103 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001104 self.issue, self.url)
1105
1106
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001107_CommentSummary = collections.namedtuple(
1108 '_CommentSummary', ['date', 'message', 'sender',
1109 # TODO(tandrii): these two aren't known in Gerrit.
1110 'approval', 'disapproval'])
1111
1112
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001113class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001114 """Changelist works with one changelist in local branch.
1115
1116 Supports two codereview backends: Rietveld or Gerrit, selected at object
1117 creation.
1118
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001119 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 16:19:37 -07001122 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001123 """
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 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001138 global settings
1139 if not settings:
1140 # Happens when git_cl.py is used as a utility library.
1141 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001142
1143 if issue:
1144 assert codereview, 'codereview must be known, if issue is known'
1145
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146 self.branchref = branchref
1147 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001148 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 self.branch = ShortBranchName(self.branchref)
1150 else:
1151 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001153 self.lookedup_issue = False
1154 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155 self.has_description = False
1156 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001157 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001159 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001160 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001161 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001162 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001163
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001164 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001165 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001166 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001167 assert self._codereview_impl
1168 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169
1170 def _load_codereview_impl(self, codereview=None, **kwargs):
1171 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001172 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1173 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1174 self._codereview = codereview
1175 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001176 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 16:19:37 -07001183 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
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001192
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
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001198 def IsGerrit(self):
1199 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001200
1201 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001202 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001203
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001204 The return value is a string suitable for passing to git cl with the --cc
1205 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001206 """
1207 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001208 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001209 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001210 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1211 return self.cc
1212
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001213 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 08:12:37 -08001216 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001217 return self.cc
1218
Daniel Cheng7227d212017-11-17 08:12:37 -08001219 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)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222
1223 def GetBranch(self):
1224 """Returns the short branch name, e.g. 'master'."""
1225 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001226 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001227 if not branchref:
1228 return None
1229 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230 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
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001238 def ClearBranch(self):
1239 """Clears cached branch data of this object."""
1240 self.branch = self.branchref = None
1241
tandrii5d48c322016-08-18 16:19:37 -07001242 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
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001257 @staticmethod
1258 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001259 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 e.g. 'origin', 'refs/heads/master'
1261 """
1262 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001263 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1264
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001266 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001268 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1269 error_ok=True).strip()
1270 if upstream_branch:
1271 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001273 # 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'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001280 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 ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286
1287 return remote, upstream_branch
1288
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001289 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001290 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())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001294 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001295 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001296
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001297 def GetUpstreamBranch(self):
1298 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001299 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001301 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/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 self.upstream_branch = upstream_branch
1306 return self.upstream_branch
1307
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001308 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001309 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001310 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:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001319 remotes = RunGit(['remote'], error_ok=True).split()
1320 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001321 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001322 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001323 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001324 logging.warn('Could not determine which remote this change is '
1325 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001326 else:
1327 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001328 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 branch = 'HEAD'
1330 if branch.startswith('refs/remotes'):
1331 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001332 elif branch.startswith('refs/branch-heads/'):
1333 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001334 else:
1335 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001336 return self._remote
1337
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001338 def GitSanityChecks(self, upstream_git_obj):
1339 """Checks git repo status and ensures diff is from local commits."""
1340
sbc@chromium.org79706062015-01-14 21:18:12 +00001341 if upstream_git_obj is None:
1342 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001343 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001344 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001345 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001346 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001347 return False
1348
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001349 # 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 09:17:49 -07001353 print('ERROR: %s is not in the current branch. You may need to rebase '
1354 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001355 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 09:17:49 -07001370 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)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001377 return False
1378 return True
1379
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001380 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001381 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001382
1383 Returns None if it is not set.
1384 """
tandrii5d48c322016-08-18 16:19:37 -07001385 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001386
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387 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:41 +00001392 is_cached, value = self._cached_remote_url
1393 if is_cached:
1394 return value
1395
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001396 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001397 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:41 +00001404 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001405 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001407 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001408 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001409 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001410 self.issue = self._GitGetBranchConfigValue(
1411 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001412 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 return self.issue
1414
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 def GetIssueURL(self):
1416 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001417 issue = self.GetIssue()
1418 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001419 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001420 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001422 def GetDescription(self, pretty=False, force=False):
1423 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001424 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001425 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426 self.has_description = True
1427 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001428 # Set width to 72 columns + 2 space indent.
1429 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001431 lines = self.description.splitlines()
1432 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 return self.description
1434
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001435 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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001451 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001452 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001453 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001454 self.patchset = self._GitGetBranchConfigValue(
1455 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001456 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457 return self.patchset
1458
1459 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001460 """Set this branch's patchset. If patchset=0, clears the patchset."""
1461 assert self.GetBranch()
1462 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001463 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001464 else:
1465 self.patchset = int(patchset)
1466 self._GitSetBranchConfigValue(
1467 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001469 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001470 """Set this branch's issue. If issue isn't given, clears the issue."""
1471 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001473 issue = int(issue)
1474 self._GitSetBranchConfigValue(
1475 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001476 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001477 codereview_server = self._codereview_impl.GetCodereviewServer()
1478 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001479 self._GitSetBranchConfigValue(
1480 self._codereview_impl.CodereviewServerConfigKey(),
1481 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482 else:
tandrii5d48c322016-08-18 16:19:37 -07001483 # 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 11:16:02 -07001492 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')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001498 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001499 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500
dnjba1b0f32016-09-02 12:37:42 -07001501 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001502 if not self.GitSanityChecks(upstream_branch):
1503 DieWithError('\nGit sanity check failure')
1504
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001505 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001506 if not root:
1507 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001508 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001509
1510 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001511 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001512 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001513 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001514 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001515 except subprocess2.CalledProcessError:
1516 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001517 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001518 'This branch probably doesn\'t exist anymore. To reset the\n'
1519 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001520 ' git branch --set-upstream-to origin/master %s\n'
1521 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001522 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001523
maruel@chromium.org52424302012-08-29 15:14:30 +00001524 issue = self.GetIssue()
1525 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001526 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001527 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.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001532 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1533 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001534
1535 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001536 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001537 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001538 name,
1539 description,
1540 absroot,
1541 files,
1542 issue,
1543 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001544 author,
1545 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001546
dsansomee2d6fd92016-09-08 00:10:47 -07001547 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001548 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001549 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001550 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001551
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001552 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 18:50:29 -04001575 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001576 """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 18:50:29 -04001581 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1582 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001583 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001584 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001585
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001586 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1587 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001588 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1589 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001590 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 08:22:09 -07001598 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001599
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001600 def CMDUpload(self, options, git_diff_args, orig_args):
1601 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001602 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001603 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001604 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001605 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001606 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 11:49:53 -07001614
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001615 # Fast best-effort checks to abort before running potentially
1616 # expensive hooks if uploading is likely to fail anyway. Passing these
1617 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001618 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001619 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001620
1621 # Apply watchlists on upload.
1622 change = self.GetChange(base_branch, None)
1623 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1624 files = [f.LocalPath() for f in change.AffectedFiles()]
1625 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001626 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001627
1628 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001629 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001630 # Set the reviewer list now so that presubmit checks can access it.
1631 change_description = ChangeDescription(change.FullDescriptionText())
1632 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001633 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001634 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001635 change)
1636 change.SetDescriptionText(change_description.description)
1637 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001638 may_prompt=not options.force,
1639 verbose=options.verbose,
1640 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001641 if not hook_results.should_continue():
1642 return 1
1643 if not options.reviewers and hook_results.reviewers:
1644 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001645 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001646
Aaron Gable13101a62018-02-09 13:20:41 -08001647 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001648 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001649 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001650 _git_set_branch_config_value('last-upload-hash',
1651 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001652 # 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 09:17:49 -07001663 print()
1664 print('--dependencies has been specified.')
1665 print('All dependent local branches will be re-uploaded.')
1666 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001667 # 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 12:53:57 -04001673 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
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001708 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001709 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001710
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 13:22:03 -07001715 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001716 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001717 return 0
1718 except KeyboardInterrupt:
1719 raise
1720 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001721 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001722 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001723 ' * 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 13:22:03 -07001730 # Still raise exception so that stack trace is printed.
1731 raise
1732
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001733 # Forward methods to codereview specific implementation.
1734
Aaron Gable636b13f2017-07-14 10:42:48 -07001735 def AddComment(self, message, publish=None):
1736 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001737
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001738 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001739 """Returns list of _CommentSummary for each comment.
1740
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001741 args:
1742 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001743 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001744 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001745
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001746 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 06:02:30 -07001755 def GetIssueOwner(self):
1756 """Get owner from codereview, which may differ from this checkout."""
1757 return self._codereview_impl.GetIssueOwner()
1758
Edward Lemur707d70b2018-02-07 00:50:14 +01001759 def GetReviewers(self):
1760 return self._codereview_impl.GetReviewers()
1761
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001762 def GetMostRecentPatchset(self):
1763 return self._codereview_impl.GetMostRecentPatchset()
1764
tandriide281ae2016-10-12 06:02:30 -07001765 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001766 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001767 return self._codereview_impl.CannotTriggerTryJobReason()
1768
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001769 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 07:52:02 -07001772
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001773 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
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001776 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001777 # 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)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001783
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 11:47:13 -08001808 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001809 """Fetches and returns description from the codereview server."""
1810 raise NotImplementedError()
1811
tandrii5d48c322016-08-18 16:19:37 -07001812 @classmethod
1813 def IssueConfigKey(cls):
1814 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001815 raise NotImplementedError()
1816
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001817 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001818 def PatchsetConfigKey(cls):
1819 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001820 raise NotImplementedError()
1821
tandrii5d48c322016-08-18 16:19:37 -07001822 @classmethod
1823 def CodereviewServerConfigKey(cls):
1824 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001825 raise NotImplementedError()
1826
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001827 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001828 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001829 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001830
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001831 def GetGerritObjForPresubmit(self):
1832 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1833 return None
1834
dsansomee2d6fd92016-09-08 00:10:47 -07001835 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001836 """Update the description on codereview site."""
1837 raise NotImplementedError()
1838
Aaron Gable636b13f2017-07-14 10:42:48 -07001839 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001840 """Posts a comment to the codereview site."""
1841 raise NotImplementedError()
1842
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001843 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001844 raise NotImplementedError()
1845
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001846 def CloseIssue(self):
1847 """Closes the issue."""
1848 raise NotImplementedError()
1849
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001850 def GetMostRecentPatchset(self):
1851 """Returns the most recent patchset number from the codereview site."""
1852 raise NotImplementedError()
1853
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001854 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001855 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001856 """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 08:22:09 -07001865 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001866 """
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 19:20:19 +01001875 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001876 """Best effort check that user is authenticated with codereview server.
1877
1878 Arguments:
1879 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001880 refresh: whether to attempt to refresh credentials. Ignored if not
1881 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001882 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001883 raise NotImplementedError()
1884
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001885 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001886 """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 14:59:28 +01001891
1892 Arguments:
1893 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001894 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001895 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001896
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001897 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001898 """Uploads a change to codereview."""
1899 raise NotImplementedError()
1900
Ravi Mistry31e7d562018-04-02 12:53:57 -04001901 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
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001908 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001909 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001910
1911 Issue must have been already uploaded and known.
1912 """
1913 raise NotImplementedError()
1914
tandriie113dfd2016-10-11 10:20:12 -07001915 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001916 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001917 raise NotImplementedError()
1918
tandriide281ae2016-10-12 06:02:30 -07001919 def GetIssueOwner(self):
1920 raise NotImplementedError()
1921
Edward Lemur707d70b2018-02-07 00:50:14 +01001922 def GetReviewers(self):
1923 raise NotImplementedError()
1924
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001925 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001926 raise NotImplementedError()
1927
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001928
1929class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001930
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001931 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001932 super(_RietveldChangelistImpl, self).__init__(changelist)
1933 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001934 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001935 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001936
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001937 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001938 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001939 self._props = None
1940 self._rpc_server = None
1941
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001942 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 16:19:37 -07001947 self._rietveld_server = gclient_utils.UpgradeToHttps(
1948 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001949 if not self._rietveld_server:
1950 self._rietveld_server = settings.GetDefaultServerUrl()
1951 return self._rietveld_server
1952
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001953 def EnsureAuthenticated(self, force, refresh=False):
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001954 raise NotImplementedError
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001955
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001956 def EnsureCanUploadPatchset(self, force):
1957 # No checks for Rietveld because we are deprecating Rietveld.
1958 pass
1959
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001960 def FetchDescription(self, force=False):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001961 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001962
1963 def GetMostRecentPatchset(self):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001964 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001965
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001966 def GetIssueProperties(self):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001967 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001968
tandriie113dfd2016-10-11 10:20:12 -07001969 def CannotTriggerTryJobReason(self):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00001970 raise NotImplementedError()
tandriie113dfd2016-10-11 10:20:12 -07001971
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001972 def GetTryJobProperties(self, patchset=None):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00001973 raise NotImplementedError()
tandrii8c5a3532016-11-04 07:52:02 -07001974
tandriide281ae2016-10-12 06:02:30 -07001975 def GetIssueOwner(self):
Andrii Shyshkalov1e488132018-10-16 21:37:21 +00001976 return (self.GetIssueProperties() or {}).get('owner_email')
tandriide281ae2016-10-12 06:02:30 -07001977
Edward Lemur707d70b2018-02-07 00:50:14 +01001978 def GetReviewers(self):
Andrii Shyshkalov1e488132018-10-16 21:37:21 +00001979 return (self.GetIssueProperties() or {}).get('reviewers')
Edward Lemur707d70b2018-02-07 00:50:14 +01001980
Aaron Gable636b13f2017-07-14 10:42:48 -07001981 def AddComment(self, message, publish=None):
Andrii Shyshkalov1e488132018-10-16 21:37:21 +00001982 return self.RpcServer().add_comment(self.GetIssue(), message)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001983
Andrii Shyshkalov1e488132018-10-16 21:37:21 +00001984 def GetCommentsSummary(self, _readable=True):
1985 summary = []
1986 for message in self.GetIssueProperties().get('messages', []):
1987 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
1988 summary.append(_CommentSummary(
1989 date=date,
1990 disapproval=bool(message['disapproval']),
1991 approval=bool(message['approval']),
1992 sender=message['sender'],
1993 message=message['text'],
1994 ))
1995 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001996
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001997 def GetStatus(self):
Andrii Shyshkalov1e488132018-10-16 21:37:21 +00001998 """Applies a rough heuristic to give a simple summary of an issue's review
1999 or CQ status, assuming adherence to a common workflow.
2000
2001 Returns None if no issue for this branch, or one of the following keywords:
2002 * 'error' - error from review tool (including deleted issues)
2003 * 'unsent' - not sent for review
2004 * 'waiting' - waiting for review
2005 * 'reply' - waiting for owner to reply to review
2006 * 'not lgtm' - Code-Review label has been set negatively
2007 * 'lgtm' - LGTM from at least one approved reviewer
2008 * 'commit' - in the commit queue
2009 * 'closed' - closed
2010 """
2011 if not self.GetIssue():
2012 return None
2013
2014 try:
2015 props = self.GetIssueProperties()
2016 except urllib2.HTTPError:
2017 return 'error'
2018
2019 if props.get('closed'):
2020 # Issue is closed.
2021 return 'closed'
2022 if props.get('commit') and not props.get('cq_dry_run', False):
2023 # Issue is in the commit queue.
2024 return 'commit'
2025
2026 messages = props.get('messages') or []
2027 if not messages:
2028 # No message was sent.
2029 return 'unsent'
2030
2031 if get_approving_reviewers(props):
2032 return 'lgtm'
2033 elif get_approving_reviewers(props, disapproval=True):
2034 return 'not lgtm'
2035
2036 # Skip CQ messages that don't require owner's action.
2037 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2038 if 'Dry run:' in messages[-1]['text']:
2039 messages.pop()
2040 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2041 # This message always follows prior messages from CQ,
2042 # so skip this too.
2043 messages.pop()
2044 else:
2045 # This is probably a CQ messages warranting user attention.
2046 break
2047
2048 if messages[-1]['sender'] != props.get('owner_email'):
2049 # Non-LGTM reply from non-owner and not CQ bot.
2050 return 'reply'
2051 return 'waiting'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002052
dsansomee2d6fd92016-09-08 00:10:47 -07002053 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov1e488132018-10-16 21:37:21 +00002054 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002055
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002056 def CloseIssue(self):
Andrii Shyshkalov1e488132018-10-16 21:37:21 +00002057 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002059 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002060 return self.SetFlags({flag: value})
2061
2062 def SetFlags(self, flags):
2063 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002064 """
phajdan.jr68598232016-08-10 03:28:28 -07002065 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002066 try:
tandrii4b233bd2016-07-06 03:50:29 -07002067 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002068 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002069 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002070 if e.code == 404:
2071 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2072 if e.code == 403:
2073 DieWithError(
2074 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002075 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002076 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002077
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002078 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002079 """Returns an upload.RpcServer() to access this review's rietveld instance.
2080 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002081 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002082 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002083 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002084 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002085 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002086
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002087 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002088 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002089 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002090
tandrii5d48c322016-08-18 16:19:37 -07002091 @classmethod
2092 def PatchsetConfigKey(cls):
2093 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002094
tandrii5d48c322016-08-18 16:19:37 -07002095 @classmethod
2096 def CodereviewServerConfigKey(cls):
2097 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002098
Ravi Mistry31e7d562018-04-02 12:53:57 -04002099 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2100 raise NotImplementedError()
2101
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002102 def SetCQState(self, new_state):
2103 props = self.GetIssueProperties()
2104 if props.get('private'):
2105 DieWithError('Cannot set-commit on private issue')
2106
2107 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002108 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002109 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002110 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002111 else:
tandrii4b233bd2016-07-06 03:50:29 -07002112 assert new_state == _CQState.DRY_RUN
2113 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002114
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002115 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002116 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002117 # PatchIssue should never be called with a dirty tree. It is up to the
2118 # caller to check this, but just in case we assert here since the
2119 # consequences of the caller not checking this could be dire.
2120 assert(not git_common.is_dirty_git_tree('apply'))
2121 assert(parsed_issue_arg.valid)
2122 self._changelist.issue = parsed_issue_arg.issue
2123 if parsed_issue_arg.hostname:
2124 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2125
skobes6468b902016-10-24 08:45:10 -07002126 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2127 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2128 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002129 try:
skobes6468b902016-10-24 08:45:10 -07002130 scm_obj.apply_patch(patchset_object)
2131 except Exception as e:
2132 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002133 return 1
2134
2135 # If we had an issue, commit the current state and register the issue.
2136 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002137 self.SetIssue(self.GetIssue())
2138 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002139 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2140 'patch from issue %(i)s at patchset '
2141 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2142 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002143 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002144 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002145 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002146 return 0
2147
2148 @staticmethod
2149 def ParseIssueURL(parsed_url):
2150 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2151 return None
wychen3c1c1722016-08-04 11:46:36 -07002152 # Rietveld patch: https://domain/<number>/#ps<patchset>
2153 match = re.match(r'/(\d+)/$', parsed_url.path)
2154 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2155 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002156 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002157 issue=int(match.group(1)),
2158 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002159 hostname=parsed_url.netloc,
2160 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002161 # Typical url: https://domain/<issue_number>[/[other]]
2162 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2163 if match:
skobes6468b902016-10-24 08:45:10 -07002164 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002165 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002166 hostname=parsed_url.netloc,
2167 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002168 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2169 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2170 if match:
skobes6468b902016-10-24 08:45:10 -07002171 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002172 issue=int(match.group(1)),
2173 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002174 hostname=parsed_url.netloc,
2175 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002176 return None
2177
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002178 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002179 """Upload the patch to Rietveld."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00002180 raise NotImplementedError
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002181
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002182
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002183class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002184 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002185 # auth_config is Rietveld thing, kept here to preserve interface only.
2186 super(_GerritChangelistImpl, self).__init__(changelist)
2187 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002188 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002189 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002190 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002191 # Map from change number (issue) to its detail cache.
2192 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002193
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002194 if codereview_host is not None:
2195 assert not codereview_host.startswith('https://'), codereview_host
2196 self._gerrit_host = codereview_host
2197 self._gerrit_server = 'https://%s' % codereview_host
2198
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002199 def _GetGerritHost(self):
2200 # Lazy load of configs.
2201 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002202 if self._gerrit_host and '.' not in self._gerrit_host:
2203 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2204 # This happens for internal stuff http://crbug.com/614312.
2205 parsed = urlparse.urlparse(self.GetRemoteUrl())
2206 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002207 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002208 ' Your current remote is: %s' % self.GetRemoteUrl())
2209 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2210 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002211 return self._gerrit_host
2212
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002213 def _GetGitHost(self):
2214 """Returns git host to be used when uploading change to Gerrit."""
2215 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2216
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002217 def GetCodereviewServer(self):
2218 if not self._gerrit_server:
2219 # If we're on a branch then get the server potentially associated
2220 # with that branch.
2221 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002222 self._gerrit_server = self._GitGetBranchConfigValue(
2223 self.CodereviewServerConfigKey())
2224 if self._gerrit_server:
2225 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002226 if not self._gerrit_server:
2227 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2228 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002229 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002230 parts[0] = parts[0] + '-review'
2231 self._gerrit_host = '.'.join(parts)
2232 self._gerrit_server = 'https://%s' % self._gerrit_host
2233 return self._gerrit_server
2234
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002235 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002236 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002237 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002238 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002239 logging.warn('can\'t detect Gerrit project.')
2240 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002241 project = urlparse.urlparse(remote_url).path.strip('/')
2242 if project.endswith('.git'):
2243 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00002244 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
2245 # 'a/' prefix, because 'a/' prefix is used to force authentication in
2246 # gitiles/git-over-https protocol. E.g.,
2247 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
2248 # as
2249 # https://chromium.googlesource.com/v8/v8
2250 if project.startswith('a/'):
2251 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002252 return project
2253
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002254 def _GerritChangeIdentifier(self):
2255 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2256
2257 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002258 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002259 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002260 project = self._GetGerritProject()
2261 if project:
2262 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2263 # Fall back on still unique, but less efficient change number.
2264 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002265
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002266 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002267 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002268 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002269
tandrii5d48c322016-08-18 16:19:37 -07002270 @classmethod
2271 def PatchsetConfigKey(cls):
2272 return 'gerritpatchset'
2273
2274 @classmethod
2275 def CodereviewServerConfigKey(cls):
2276 return 'gerritserver'
2277
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002278 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002279 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002280 if settings.GetGerritSkipEnsureAuthenticated():
2281 # For projects with unusual authentication schemes.
2282 # See http://crbug.com/603378.
2283 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002284
2285 # Check presence of cookies only if using cookies-based auth method.
2286 cookie_auth = gerrit_util.Authenticator.get()
2287 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002288 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002289
2290 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002291 self.GetCodereviewServer()
2292 git_host = self._GetGitHost()
2293 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002294
2295 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2296 git_auth = cookie_auth.get_auth_header(git_host)
2297 if gerrit_auth and git_auth:
2298 if gerrit_auth == git_auth:
2299 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002300 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002301 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002302 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002303 ' %s\n'
2304 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002305 ' Consider running the following command:\n'
2306 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002307 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002308 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002309 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002310 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002311 cookie_auth.get_new_password_message(git_host)))
2312 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002313 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002314 return
2315 else:
2316 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002317 ([] if gerrit_auth else [self._gerrit_host]) +
2318 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002319 DieWithError('Credentials for the following hosts are required:\n'
2320 ' %s\n'
2321 'These are read from %s (or legacy %s)\n'
2322 '%s' % (
2323 '\n '.join(missing),
2324 cookie_auth.get_gitcookies_path(),
2325 cookie_auth.get_netrc_path(),
2326 cookie_auth.get_new_password_message(git_host)))
2327
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002328 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002329 if not self.GetIssue():
2330 return
2331
2332 # Warm change details cache now to avoid RPCs later, reducing latency for
2333 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002334 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002335 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002336
2337 status = self._GetChangeDetail()['status']
2338 if status in ('MERGED', 'ABANDONED'):
2339 DieWithError('Change %s has been %s, new uploads are not allowed' %
2340 (self.GetIssueURL(),
2341 'submitted' if status == 'MERGED' else 'abandoned'))
2342
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002343 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2344 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2345 # Apparently this check is not very important? Otherwise get_auth_email
2346 # could have been added to other implementations of Authenticator.
2347 cookies_auth = gerrit_util.Authenticator.get()
2348 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002349 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002350
2351 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002352 if self.GetIssueOwner() == cookies_user:
2353 return
2354 logging.debug('change %s owner is %s, cookies user is %s',
2355 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002356 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002357 # so ask what Gerrit thinks of this user.
2358 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2359 if details['email'] == self.GetIssueOwner():
2360 return
2361 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002362 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002363 'as %s.\n'
2364 'Uploading may fail due to lack of permissions.' %
2365 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2366 confirm_or_exit(action='upload')
2367
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002368 def _PostUnsetIssueProperties(self):
2369 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002370 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002371
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002372 def GetGerritObjForPresubmit(self):
2373 return presubmit_support.GerritAccessor(self._GetGerritHost())
2374
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002375 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002376 """Apply a rough heuristic to give a simple summary of an issue's review
2377 or CQ status, assuming adherence to a common workflow.
2378
2379 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002380 * 'error' - error from review tool (including deleted issues)
2381 * 'unsent' - no reviewers added
2382 * 'waiting' - waiting for review
2383 * 'reply' - waiting for uploader to reply to review
2384 * 'lgtm' - Code-Review label has been set
2385 * 'commit' - in the commit queue
2386 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002387 """
2388 if not self.GetIssue():
2389 return None
2390
2391 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002392 data = self._GetChangeDetail([
2393 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002394 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002395 return 'error'
2396
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002397 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002398 return 'closed'
2399
Aaron Gable9ab38c62017-04-06 14:36:33 -07002400 if data['labels'].get('Commit-Queue', {}).get('approved'):
2401 # The section will have an "approved" subsection if anyone has voted
2402 # the maximum value on the label.
2403 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002404
Aaron Gable9ab38c62017-04-06 14:36:33 -07002405 if data['labels'].get('Code-Review', {}).get('approved'):
2406 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002407
2408 if not data.get('reviewers', {}).get('REVIEWER', []):
2409 return 'unsent'
2410
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002411 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002412 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2413 last_message_author = messages.pop().get('author', {})
2414 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002415 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2416 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002417 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002418 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002419 if last_message_author.get('_account_id') == owner:
2420 # Most recent message was by owner.
2421 return 'waiting'
2422 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002423 # Some reply from non-owner.
2424 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002425
2426 # Somehow there are no messages even though there are reviewers.
2427 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002428
2429 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002430 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002431 patchset = data['revisions'][data['current_revision']]['_number']
2432 self.SetPatchset(patchset)
2433 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002434
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002435 def FetchDescription(self, force=False):
2436 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2437 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002438 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002439 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002440
dsansomee2d6fd92016-09-08 00:10:47 -07002441 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002442 if gerrit_util.HasPendingChangeEdit(
2443 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002444 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002445 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002446 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002447 'unpublished edit. Either publish the edit in the Gerrit web UI '
2448 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002449
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002450 gerrit_util.DeletePendingChangeEdit(
2451 self._GetGerritHost(), self._GerritChangeIdentifier())
2452 gerrit_util.SetCommitMessage(
2453 self._GetGerritHost(), self._GerritChangeIdentifier(),
2454 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002455
Aaron Gable636b13f2017-07-14 10:42:48 -07002456 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002457 gerrit_util.SetReview(
2458 self._GetGerritHost(), self._GerritChangeIdentifier(),
2459 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002460
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002461 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002462 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002463 messages = self._GetChangeDetail(
2464 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2465 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002466 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002467
2468 # Build dictionary of file comments for easy access and sorting later.
2469 # {author+date: {path: {patchset: {line: url+message}}}}
2470 comments = collections.defaultdict(
2471 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2472 for path, line_comments in file_comments.iteritems():
2473 for comment in line_comments:
2474 if comment.get('tag', '').startswith('autogenerated'):
2475 continue
2476 key = (comment['author']['email'], comment['updated'])
2477 if comment.get('side', 'REVISION') == 'PARENT':
2478 patchset = 'Base'
2479 else:
2480 patchset = 'PS%d' % comment['patch_set']
2481 line = comment.get('line', 0)
2482 url = ('https://%s/c/%s/%s/%s#%s%s' %
2483 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2484 'b' if comment.get('side') == 'PARENT' else '',
2485 str(line) if line else ''))
2486 comments[key][path][patchset][line] = (url, comment['message'])
2487
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002488 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002489 for msg in messages:
2490 # Don't bother showing autogenerated messages.
2491 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2492 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002493 # Gerrit spits out nanoseconds.
2494 assert len(msg['date'].split('.')[-1]) == 9
2495 date = datetime.datetime.strptime(msg['date'][:-3],
2496 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002497 message = msg['message']
2498 key = (msg['author']['email'], msg['date'])
2499 if key in comments:
2500 message += '\n'
2501 for path, patchsets in sorted(comments.get(key, {}).items()):
2502 if readable:
2503 message += '\n%s' % path
2504 for patchset, lines in sorted(patchsets.items()):
2505 for line, (url, content) in sorted(lines.items()):
2506 if line:
2507 line_str = 'Line %d' % line
2508 path_str = '%s:%d:' % (path, line)
2509 else:
2510 line_str = 'File comment'
2511 path_str = '%s:0:' % path
2512 if readable:
2513 message += '\n %s, %s: %s' % (patchset, line_str, url)
2514 message += '\n %s\n' % content
2515 else:
2516 message += '\n%s ' % path_str
2517 message += '\n%s\n' % content
2518
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002519 summary.append(_CommentSummary(
2520 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002521 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002522 sender=msg['author']['email'],
2523 # These could be inferred from the text messages and correlated with
2524 # Code-Review label maximum, however this is not reliable.
2525 # Leaving as is until the need arises.
2526 approval=False,
2527 disapproval=False,
2528 ))
2529 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002530
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002531 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002532 gerrit_util.AbandonChange(
2533 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002534
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002535 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002536 gerrit_util.SubmitChange(
2537 self._GetGerritHost(), self._GerritChangeIdentifier(),
2538 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002539
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002540 def _GetChangeDetail(self, options=None, no_cache=False):
2541 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002542
2543 If fresh data is needed, set no_cache=True which will clear cache and
2544 thus new data will be fetched from Gerrit.
2545 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002546 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002547 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002548
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002549 # Optimization to avoid multiple RPCs:
2550 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2551 'CURRENT_COMMIT' not in options):
2552 options.append('CURRENT_COMMIT')
2553
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002554 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002555 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002556 options = [o.upper() for o in options]
2557
2558 # Check in cache first unless no_cache is True.
2559 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002560 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002561 else:
2562 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002563 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002564 # Assumption: data fetched before with extra options is suitable
2565 # for return for a smaller set of options.
2566 # For example, if we cached data for
2567 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2568 # and request is for options=[CURRENT_REVISION],
2569 # THEN we can return prior cached data.
2570 if options_set.issubset(cached_options_set):
2571 return data
2572
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002573 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002574 data = gerrit_util.GetChangeDetail(
2575 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002576 except gerrit_util.GerritError as e:
2577 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002578 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002579 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002580
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002581 self._detail_cache.setdefault(cache_key, []).append(
2582 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002583 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002584
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002585 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002586 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002587 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002588 data = gerrit_util.GetChangeCommit(
2589 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002590 except gerrit_util.GerritError as e:
2591 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002592 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002593 raise
agable32978d92016-11-01 12:55:02 -07002594 return data
2595
Olivier Robin75ee7252018-04-13 10:02:56 +02002596 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002597 if git_common.is_dirty_git_tree('land'):
2598 return 1
tandriid60367b2016-06-22 05:25:12 -07002599 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2600 if u'Commit-Queue' in detail.get('labels', {}):
2601 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002602 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2603 'which can test and land changes for you. '
2604 'Are you sure you wish to bypass it?\n',
2605 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002606
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002607 differs = True
tandriic4344b52016-08-29 06:04:54 -07002608 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002609 # Note: git diff outputs nothing if there is no diff.
2610 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002611 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002612 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002613 if detail['current_revision'] == last_upload:
2614 differs = False
2615 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002616 print('WARNING: Local branch contents differ from latest uploaded '
2617 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002618 if differs:
2619 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002620 confirm_or_exit(
2621 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2622 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002623 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002624 elif not bypass_hooks:
2625 hook_results = self.RunHook(
2626 committing=True,
2627 may_prompt=not force,
2628 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002629 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2630 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002631 if not hook_results.should_continue():
2632 return 1
2633
2634 self.SubmitIssue(wait_for_merge=True)
2635 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002636 links = self._GetChangeCommit().get('web_links', [])
2637 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002638 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002639 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002640 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002641 return 0
2642
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002643 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002644 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002645 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002646 assert not directory
2647 assert parsed_issue_arg.valid
2648
2649 self._changelist.issue = parsed_issue_arg.issue
2650
2651 if parsed_issue_arg.hostname:
2652 self._gerrit_host = parsed_issue_arg.hostname
2653 self._gerrit_server = 'https://%s' % self._gerrit_host
2654
tandriic2405f52016-10-10 08:13:15 -07002655 try:
2656 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002657 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002658 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002659
2660 if not parsed_issue_arg.patchset:
2661 # Use current revision by default.
2662 revision_info = detail['revisions'][detail['current_revision']]
2663 patchset = int(revision_info['_number'])
2664 else:
2665 patchset = parsed_issue_arg.patchset
2666 for revision_info in detail['revisions'].itervalues():
2667 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2668 break
2669 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002670 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002671 (parsed_issue_arg.patchset, self.GetIssue()))
2672
Aaron Gable697a91b2018-01-19 15:20:15 -08002673 remote_url = self._changelist.GetRemoteUrl()
2674 if remote_url.endswith('.git'):
2675 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002676 remote_url = remote_url.rstrip('/')
2677
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002678 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002679 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002680
2681 if remote_url != fetch_info['url']:
2682 DieWithError('Trying to patch a change from %s but this repo appears '
2683 'to be %s.' % (fetch_info['url'], remote_url))
2684
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002685 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002686
Aaron Gable62619a32017-06-16 08:22:09 -07002687 if force:
2688 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2689 print('Checked out commit for change %i patchset %i locally' %
2690 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002691 elif nocommit:
2692 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2693 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002694 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002695 RunGit(['cherry-pick', 'FETCH_HEAD'])
2696 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002697 (parsed_issue_arg.issue, patchset))
2698 print('Note: this created a local commit which does not have '
2699 'the same hash as the one uploaded for review. This will make '
2700 'uploading changes based on top of this branch difficult.\n'
2701 'If you want to do that, use "git cl patch --force" instead.')
2702
Stefan Zagerd08043c2017-10-12 12:07:02 -07002703 if self.GetBranch():
2704 self.SetIssue(parsed_issue_arg.issue)
2705 self.SetPatchset(patchset)
2706 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2707 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2708 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2709 else:
2710 print('WARNING: You are in detached HEAD state.\n'
2711 'The patch has been applied to your checkout, but you will not be '
2712 'able to upload a new patch set to the gerrit issue.\n'
2713 'Try using the \'-b\' option if you would like to work on a '
2714 'branch and/or upload a new patch set.')
2715
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002716 return 0
2717
2718 @staticmethod
2719 def ParseIssueURL(parsed_url):
2720 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2721 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002722 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2723 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002724 # Short urls like https://domain/<issue_number> can be used, but don't allow
2725 # specifying the patchset (you'd 404), but we allow that here.
2726 if parsed_url.path == '/':
2727 part = parsed_url.fragment
2728 else:
2729 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002730 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002731 if match:
2732 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002733 issue=int(match.group(3)),
2734 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002735 hostname=parsed_url.netloc,
2736 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002737 return None
2738
tandrii16e0b4e2016-06-07 10:34:28 -07002739 def _GerritCommitMsgHookCheck(self, offer_removal):
2740 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2741 if not os.path.exists(hook):
2742 return
2743 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2744 # custom developer made one.
2745 data = gclient_utils.FileRead(hook)
2746 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2747 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002748 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002749 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002750 'and may interfere with it in subtle ways.\n'
2751 'We recommend you remove the commit-msg hook.')
2752 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002753 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002754 gclient_utils.rm_file_or_tree(hook)
2755 print('Gerrit commit-msg hook removed.')
2756 else:
2757 print('OK, will keep Gerrit commit-msg hook in place.')
2758
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002759 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002760 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002761 if options.squash and options.no_squash:
2762 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002763
2764 if not options.squash and not options.no_squash:
2765 # Load default for user, repo, squash=true, in this order.
2766 options.squash = settings.GetSquashGerritUploads()
2767 elif options.no_squash:
2768 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002769
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002770 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002771 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002772
Aaron Gableb56ad332017-01-06 15:24:31 -08002773 # This may be None; default fallback value is determined in logic below.
2774 title = options.title
2775
Dominic Battre7d1c4842017-10-27 09:17:28 +02002776 # Extract bug number from branch name.
2777 bug = options.bug
2778 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2779 if not bug and match:
2780 bug = match.group(1)
2781
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002782 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002783 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002784 if self.GetIssue():
2785 # Try to get the message from a previous upload.
2786 message = self.GetDescription()
2787 if not message:
2788 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002789 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002790 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002791 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002792 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002793 # When uploading a subsequent patchset, -m|--message is taken
2794 # as the patchset title if --title was not provided.
2795 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002796 else:
2797 default_title = RunGit(
2798 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002799 if options.force:
2800 title = default_title
2801 else:
2802 title = ask_for_data(
2803 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002804 change_id = self._GetChangeDetail()['change_id']
2805 while True:
2806 footer_change_ids = git_footers.get_footer_change_id(message)
2807 if footer_change_ids == [change_id]:
2808 break
2809 if not footer_change_ids:
2810 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002811 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002812 continue
2813 # There is already a valid footer but with different or several ids.
2814 # Doing this automatically is non-trivial as we don't want to lose
2815 # existing other footers, yet we want to append just 1 desired
2816 # Change-Id. Thus, just create a new footer, but let user verify the
2817 # new description.
2818 message = '%s\n\nChange-Id: %s' % (message, change_id)
2819 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002820 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002821 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002822 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002823 'Please, check the proposed correction to the description, '
2824 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2825 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2826 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002827 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002828 if not options.force:
2829 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002830 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002831 message = change_desc.description
2832 if not message:
2833 DieWithError("Description is empty. Aborting...")
2834 # Continue the while loop.
2835 # Sanity check of this code - we should end up with proper message
2836 # footer.
2837 assert [change_id] == git_footers.get_footer_change_id(message)
2838 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002839 else: # if not self.GetIssue()
2840 if options.message:
2841 message = options.message
2842 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002843 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002844 if options.title:
2845 message = options.title + '\n\n' + message
2846 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002847
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002848 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002849 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002850 # On first upload, patchset title is always this string, while
2851 # --title flag gets converted to first line of message.
2852 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002853 if not change_desc.description:
2854 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002855 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002856 if len(change_ids) > 1:
2857 DieWithError('too many Change-Id footers, at most 1 allowed.')
2858 if not change_ids:
2859 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002860 change_desc.set_description(git_footers.add_footer_change_id(
2861 change_desc.description,
2862 GenerateGerritChangeId(change_desc.description)))
2863 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002864 assert len(change_ids) == 1
2865 change_id = change_ids[0]
2866
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002867 if options.reviewers or options.tbrs or options.add_owners_to:
2868 change_desc.update_reviewers(options.reviewers, options.tbrs,
2869 options.add_owners_to, change)
2870
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002871 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002872 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2873 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002874 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002875 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2876 desc_tempfile.write(change_desc.description)
2877 desc_tempfile.close()
2878 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2879 '-F', desc_tempfile.name]).strip()
2880 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002881 else:
2882 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002883 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002884 if not change_desc.description:
2885 DieWithError("Description is empty. Aborting...")
2886
2887 if not git_footers.get_footer_change_id(change_desc.description):
2888 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002889 change_desc.set_description(
2890 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002891 if options.reviewers or options.tbrs or options.add_owners_to:
2892 change_desc.update_reviewers(options.reviewers, options.tbrs,
2893 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002894 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002895 # For no-squash mode, we assume the remote called "origin" is the one we
2896 # want. It is not worthwhile to support different workflows for
2897 # no-squash mode.
2898 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002899 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2900
2901 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002902 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002903 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2904 ref_to_push)]).splitlines()
2905 if len(commits) > 1:
2906 print('WARNING: This will upload %d commits. Run the following command '
2907 'to see which commits will be uploaded: ' % len(commits))
2908 print('git log %s..%s' % (parent, ref_to_push))
2909 print('You can also use `git squash-branch` to squash these into a '
2910 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002911 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002912
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002913 if options.reviewers or options.tbrs or options.add_owners_to:
2914 change_desc.update_reviewers(options.reviewers, options.tbrs,
2915 options.add_owners_to, change)
2916
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002917 reviewers = sorted(change_desc.get_reviewers())
2918 # Add cc's from the CC_LIST and --cc flag (if any).
2919 if not options.private and not options.no_autocc:
2920 cc = self.GetCCList().split(',')
2921 else:
2922 cc = []
2923 if options.cc:
2924 cc.extend(options.cc)
2925 cc = filter(None, [email.strip() for email in cc])
2926 if change_desc.get_cced():
2927 cc.extend(change_desc.get_cced())
Andrii Shyshkalovba7b0a42018-10-15 03:20:35 +00002928 valid_accounts = gerrit_util.ValidAccounts(
2929 self._GetGerritHost(), reviewers + cc)
2930 logging.debug('accounts %s are valid, %s invalid', sorted(valid_accounts),
2931 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002932
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002933 # Extra options that can be specified at push time. Doc:
2934 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002935 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002936
Aaron Gable844cf292017-06-28 11:32:59 -07002937 # By default, new changes are started in WIP mode, and subsequent patchsets
2938 # don't send email. At any time, passing --send-mail will mark the change
2939 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002940 if options.send_mail:
2941 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002942 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002943 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002944 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002945 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002946 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002947
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002948 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002949 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002950
Aaron Gable9b713dd2016-12-14 16:04:21 -08002951 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002952 # Punctuation and whitespace in |title| must be percent-encoded.
2953 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002954
agablec6787972016-09-09 16:13:34 -07002955 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002956 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002957
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002958 for r in sorted(reviewers):
2959 if r in valid_accounts:
2960 refspec_opts.append('r=%s' % r)
2961 reviewers.remove(r)
2962 else:
2963 # TODO(tandrii): this should probably be a hard failure.
2964 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2965 % r)
2966 for c in sorted(cc):
2967 # refspec option will be rejected if cc doesn't correspond to an
2968 # account, even though REST call to add such arbitrary cc may succeed.
2969 if c in valid_accounts:
2970 refspec_opts.append('cc=%s' % c)
2971 cc.remove(c)
2972
2973
rmistry9eadede2016-09-19 11:22:43 -07002974 if options.topic:
2975 # Documentation on Gerrit topics is here:
2976 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002977 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002978
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002979 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002980 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002981 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002982 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002983 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2984
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002985 refspec_suffix = ''
2986 if refspec_opts:
2987 refspec_suffix = '%' + ','.join(refspec_opts)
2988 assert ' ' not in refspec_suffix, (
2989 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2990 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2991
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002992 try:
Edward Lemur83bd7f42018-10-10 00:14:21 +00002993 # TODO(crbug.com/881860): Remove.
Edward Lemur47faa062018-10-11 19:46:02 +00002994 # Clear the log after each git-cl upload run by setting mode='w'.
2995 handler = logging.FileHandler(gerrit_util.GERRIT_ERR_LOG_FILE, mode='w')
2996 handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
2997
2998 GERRIT_ERR_LOGGER.addHandler(handler)
2999 GERRIT_ERR_LOGGER.setLevel(logging.INFO)
3000 # Don't propagate to root logger, so that logs are not printed.
3001 GERRIT_ERR_LOGGER.propagate = 0
3002
Edward Lemur83bd7f42018-10-10 00:14:21 +00003003 # Get interesting headers from git push, to be displayed to the user if
3004 # subsequent Gerrit RPC calls fail.
3005 env = os.environ.copy()
3006 env['GIT_CURL_VERBOSE'] = '1'
3007 class FilterHeaders(object):
3008 """Filter git push headers and store them in a file.
3009
3010 Regular git push output is printed directly.
3011 """
3012
3013 def __init__(self):
3014 # The output from git push that we want to store in a file.
3015 self._output = ''
3016 # Keeps track of whether the current line is part of a request header.
3017 self._on_header = False
3018 # Keeps track of repeated empty lines, which mark the end of a request
3019 # header.
3020 self._last_line_empty = False
3021
3022 def __call__(self, line):
3023 """Handle a single line of git push output."""
3024 if not line:
3025 # Two consecutive empty lines mark the end of a header.
3026 if self._last_line_empty:
3027 self._on_header = False
3028 self._last_line_empty = True
3029 return
3030
3031 self._last_line_empty = False
3032 # A line starting with '>' marks the beggining of a request header.
3033 if line[0] == '>':
3034 self._on_header = True
3035 GERRIT_ERR_LOGGER.info(line)
3036 # Lines not starting with '*' or '<', and not part of a request header
3037 # should be displayed to the user.
3038 elif line[0] not in '*<' and not self._on_header:
3039 print(line)
3040 # Flush after every line: useful for seeing progress when running as
3041 # recipe.
3042 sys.stdout.flush()
3043 # Filter out the cookie and authorization headers.
3044 elif ('cookie: ' not in line.lower()
3045 and 'authorization: ' not in line.lower()):
3046 GERRIT_ERR_LOGGER.info(line)
3047
3048 filter_fn = FilterHeaders()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003049 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00003050 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemur83bd7f42018-10-10 00:14:21 +00003051 print_stdout=False,
3052 filter_fn=filter_fn,
3053 env=env)
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003054 except subprocess2.CalledProcessError:
3055 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003056 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003057 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003058 'credential problems:\n'
3059 ' git cl creds-check\n',
3060 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003061
3062 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003063 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003064 change_numbers = [m.group(1)
3065 for m in map(regex.match, push_stdout.splitlines())
3066 if m]
3067 if len(change_numbers) != 1:
3068 DieWithError(
3069 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003070 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003071 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003072 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003073
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00003074 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003075 # GetIssue() is not set in case of non-squash uploads according to tests.
3076 # TODO(agable): non-squash uploads in git cl should be removed.
3077 gerrit_util.AddReviewers(
3078 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003079 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003080 reviewers, cc,
3081 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003082
Aaron Gablefd238082017-06-07 13:42:34 -07003083 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003084 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3085 score = 1
3086 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3087 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3088 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003089 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003090 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003091 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003092 msg='Self-approving for TBR',
3093 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003094
Andrii Shyshkalovdd788442018-10-13 17:55:29 +00003095 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
3096 options.cq_dry_run)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003097 return 0
3098
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003099 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3100 change_desc):
3101 """Computes parent of the generated commit to be uploaded to Gerrit.
3102
3103 Returns revision or a ref name.
3104 """
3105 if custom_cl_base:
3106 # Try to avoid creating additional unintended CLs when uploading, unless
3107 # user wants to take this risk.
3108 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3109 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3110 local_ref_of_target_remote])
3111 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003112 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003113 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3114 'If you proceed with upload, more than 1 CL may be created by '
3115 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3116 'If you are certain that specified base `%s` has already been '
3117 'uploaded to Gerrit as another CL, you may proceed.\n' %
3118 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3119 if not force:
3120 confirm_or_exit(
3121 'Do you take responsibility for cleaning up potential mess '
3122 'resulting from proceeding with upload?',
3123 action='upload')
3124 return custom_cl_base
3125
Aaron Gablef97e33d2017-03-30 15:44:27 -07003126 if remote != '.':
3127 return self.GetCommonAncestorWithUpstream()
3128
3129 # If our upstream branch is local, we base our squashed commit on its
3130 # squashed version.
3131 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3132
Aaron Gablef97e33d2017-03-30 15:44:27 -07003133 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003134 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003135
3136 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003137 # TODO(tandrii): consider checking parent change in Gerrit and using its
3138 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3139 # the tree hash of the parent branch. The upside is less likely bogus
3140 # requests to reupload parent change just because it's uploadhash is
3141 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003142 parent = RunGit(['config',
3143 'branch.%s.gerritsquashhash' % upstream_branch_name],
3144 error_ok=True).strip()
3145 # Verify that the upstream branch has been uploaded too, otherwise
3146 # Gerrit will create additional CLs when uploading.
3147 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3148 RunGitSilent(['rev-parse', parent + ':'])):
3149 DieWithError(
3150 '\nUpload upstream branch %s first.\n'
3151 'It is likely that this branch has been rebased since its last '
3152 'upload, so you just need to upload it again.\n'
3153 '(If you uploaded it with --no-squash, then branch dependencies '
3154 'are not supported, and you should reupload with --squash.)'
3155 % upstream_branch_name,
3156 change_desc)
3157 return parent
3158
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003159 def _AddChangeIdToCommitMessage(self, options, args):
3160 """Re-commits using the current message, assumes the commit hook is in
3161 place.
3162 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003163 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003164 git_command = ['commit', '--amend', '-m', log_desc]
3165 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003166 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003167 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003168 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003169 return new_log_desc
3170 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003171 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003172
Ravi Mistry31e7d562018-04-02 12:53:57 -04003173 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3174 """Sets labels on the change based on the provided flags."""
3175 labels = {}
3176 notify = None;
3177 if enable_auto_submit:
3178 labels['Auto-Submit'] = 1
3179 if use_commit_queue:
3180 labels['Commit-Queue'] = 2
3181 elif cq_dry_run:
3182 labels['Commit-Queue'] = 1
3183 notify = False
3184 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003185 gerrit_util.SetReview(
3186 self._GetGerritHost(),
3187 self._GerritChangeIdentifier(),
3188 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003189
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003190 def SetCQState(self, new_state):
3191 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003192 vote_map = {
3193 _CQState.NONE: 0,
3194 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003195 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003196 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003197 labels = {'Commit-Queue': vote_map[new_state]}
3198 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003199 gerrit_util.SetReview(
3200 self._GetGerritHost(), self._GerritChangeIdentifier(),
3201 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003202
tandriie113dfd2016-10-11 10:20:12 -07003203 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003204 try:
3205 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003206 except GerritChangeNotExists:
3207 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003208
3209 if data['status'] in ('ABANDONED', 'MERGED'):
3210 return 'CL %s is closed' % self.GetIssue()
3211
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003212 def GetTryJobProperties(self, patchset=None):
3213 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003214 data = self._GetChangeDetail(['ALL_REVISIONS'])
3215 patchset = int(patchset or self.GetPatchset())
3216 assert patchset
3217 revision_data = None # Pylint wants it to be defined.
3218 for revision_data in data['revisions'].itervalues():
3219 if int(revision_data['_number']) == patchset:
3220 break
3221 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003222 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003223 (patchset, self.GetIssue()))
3224 return {
3225 'patch_issue': self.GetIssue(),
3226 'patch_set': patchset or self.GetPatchset(),
3227 'patch_project': data['project'],
3228 'patch_storage': 'gerrit',
3229 'patch_ref': revision_data['fetch']['http']['ref'],
3230 'patch_repository_url': revision_data['fetch']['http']['url'],
3231 'patch_gerrit_url': self.GetCodereviewServer(),
3232 }
tandriie113dfd2016-10-11 10:20:12 -07003233
tandriide281ae2016-10-12 06:02:30 -07003234 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003235 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003236
Edward Lemur707d70b2018-02-07 00:50:14 +01003237 def GetReviewers(self):
3238 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3239 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3240
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003241
3242_CODEREVIEW_IMPLEMENTATIONS = {
3243 'rietveld': _RietveldChangelistImpl,
3244 'gerrit': _GerritChangelistImpl,
3245}
3246
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003247
iannuccie53c9352016-08-17 14:40:40 -07003248def _add_codereview_issue_select_options(parser, extra=""):
3249 _add_codereview_select_options(parser)
3250
3251 text = ('Operate on this issue number instead of the current branch\'s '
3252 'implicit issue.')
3253 if extra:
3254 text += ' '+extra
3255 parser.add_option('-i', '--issue', type=int, help=text)
3256
3257
3258def _process_codereview_issue_select_options(parser, options):
3259 _process_codereview_select_options(parser, options)
3260 if options.issue is not None and not options.forced_codereview:
3261 parser.error('--issue must be specified with either --rietveld or --gerrit')
3262
3263
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003264def _add_codereview_select_options(parser):
3265 """Appends --gerrit and --rietveld options to force specific codereview."""
3266 parser.codereview_group = optparse.OptionGroup(
3267 parser, 'EXPERIMENTAL! Codereview override options')
3268 parser.add_option_group(parser.codereview_group)
3269 parser.codereview_group.add_option(
3270 '--gerrit', action='store_true',
3271 help='Force the use of Gerrit for codereview')
3272 parser.codereview_group.add_option(
3273 '--rietveld', action='store_true',
3274 help='Force the use of Rietveld for codereview')
3275
3276
3277def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003278 if options.rietveld:
3279 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003280 options.forced_codereview = None
3281 if options.gerrit:
3282 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003283
3284
tandriif9aefb72016-07-01 09:06:51 -07003285def _get_bug_line_values(default_project, bugs):
3286 """Given default_project and comma separated list of bugs, yields bug line
3287 values.
3288
3289 Each bug can be either:
3290 * a number, which is combined with default_project
3291 * string, which is left as is.
3292
3293 This function may produce more than one line, because bugdroid expects one
3294 project per line.
3295
3296 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3297 ['v8:123', 'chromium:789']
3298 """
3299 default_bugs = []
3300 others = []
3301 for bug in bugs.split(','):
3302 bug = bug.strip()
3303 if bug:
3304 try:
3305 default_bugs.append(int(bug))
3306 except ValueError:
3307 others.append(bug)
3308
3309 if default_bugs:
3310 default_bugs = ','.join(map(str, default_bugs))
3311 if default_project:
3312 yield '%s:%s' % (default_project, default_bugs)
3313 else:
3314 yield default_bugs
3315 for other in sorted(others):
3316 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3317 yield other
3318
3319
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003320class ChangeDescription(object):
3321 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003322 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003323 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003324 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003325 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003326 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3327 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3328 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3329 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003330
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003331 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003332 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003333
agable@chromium.org42c20792013-09-12 17:34:49 +00003334 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003335 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003336 return '\n'.join(self._description_lines)
3337
3338 def set_description(self, desc):
3339 if isinstance(desc, basestring):
3340 lines = desc.splitlines()
3341 else:
3342 lines = [line.rstrip() for line in desc]
3343 while lines and not lines[0]:
3344 lines.pop(0)
3345 while lines and not lines[-1]:
3346 lines.pop(-1)
3347 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003348
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003349 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3350 """Rewrites the R=/TBR= line(s) as a single line each.
3351
3352 Args:
3353 reviewers (list(str)) - list of additional emails to use for reviewers.
3354 tbrs (list(str)) - list of additional emails to use for TBRs.
3355 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3356 the change that are missing OWNER coverage. If this is not None, you
3357 must also pass a value for `change`.
3358 change (Change) - The Change that should be used for OWNERS lookups.
3359 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003360 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003361 assert isinstance(tbrs, list), tbrs
3362
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003363 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003364 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003365
3366 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003367 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003368
3369 reviewers = set(reviewers)
3370 tbrs = set(tbrs)
3371 LOOKUP = {
3372 'TBR': tbrs,
3373 'R': reviewers,
3374 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003375
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003376 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003377 regexp = re.compile(self.R_LINE)
3378 matches = [regexp.match(line) for line in self._description_lines]
3379 new_desc = [l for i, l in enumerate(self._description_lines)
3380 if not matches[i]]
3381 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003382
agable@chromium.org42c20792013-09-12 17:34:49 +00003383 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003384
3385 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003386 for match in matches:
3387 if not match:
3388 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003389 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3390
3391 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003392 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003393 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003394 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003395 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003396 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003397 LOOKUP[add_owners_to].update(
3398 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003399
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003400 # If any folks ended up in both groups, remove them from tbrs.
3401 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003402
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003403 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3404 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003405
3406 # Put the new lines in the description where the old first R= line was.
3407 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3408 if 0 <= line_loc < len(self._description_lines):
3409 if new_tbr_line:
3410 self._description_lines.insert(line_loc, new_tbr_line)
3411 if new_r_line:
3412 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003413 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003414 if new_r_line:
3415 self.append_footer(new_r_line)
3416 if new_tbr_line:
3417 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003418
Aaron Gable3a16ed12017-03-23 10:51:55 -07003419 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003420 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003421 self.set_description([
3422 '# Enter a description of the change.',
3423 '# This will be displayed on the codereview site.',
3424 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003425 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003426 '--------------------',
3427 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003428
agable@chromium.org42c20792013-09-12 17:34:49 +00003429 regexp = re.compile(self.BUG_LINE)
3430 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003431 prefix = settings.GetBugPrefix()
3432 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003433 if git_footer:
3434 self.append_footer('Bug: %s' % ', '.join(values))
3435 else:
3436 for value in values:
3437 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003438
agable@chromium.org42c20792013-09-12 17:34:49 +00003439 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003440 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003441 if not content:
3442 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003443 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003444
Bruce Dawson2377b012018-01-11 16:46:49 -08003445 # Strip off comments and default inserted "Bug:" line.
3446 clean_lines = [line.rstrip() for line in lines if not
3447 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003448 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003449 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003450 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003451
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003452 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003453 """Adds a footer line to the description.
3454
3455 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3456 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3457 that Gerrit footers are always at the end.
3458 """
3459 parsed_footer_line = git_footers.parse_footer(line)
3460 if parsed_footer_line:
3461 # Line is a gerrit footer in the form: Footer-Key: any value.
3462 # Thus, must be appended observing Gerrit footer rules.
3463 self.set_description(
3464 git_footers.add_footer(self.description,
3465 key=parsed_footer_line[0],
3466 value=parsed_footer_line[1]))
3467 return
3468
3469 if not self._description_lines:
3470 self._description_lines.append(line)
3471 return
3472
3473 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3474 if gerrit_footers:
3475 # git_footers.split_footers ensures that there is an empty line before
3476 # actual (gerrit) footers, if any. We have to keep it that way.
3477 assert top_lines and top_lines[-1] == ''
3478 top_lines, separator = top_lines[:-1], top_lines[-1:]
3479 else:
3480 separator = [] # No need for separator if there are no gerrit_footers.
3481
3482 prev_line = top_lines[-1] if top_lines else ''
3483 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3484 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3485 top_lines.append('')
3486 top_lines.append(line)
3487 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003488
tandrii99a72f22016-08-17 14:33:24 -07003489 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003490 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003491 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003492 reviewers = [match.group(2).strip()
3493 for match in matches
3494 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003495 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003496
bradnelsond975b302016-10-23 12:20:23 -07003497 def get_cced(self):
3498 """Retrieves the list of reviewers."""
3499 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3500 cced = [match.group(2).strip() for match in matches if match]
3501 return cleanup_list(cced)
3502
Nodir Turakulov23b82142017-11-16 11:04:25 -08003503 def get_hash_tags(self):
3504 """Extracts and sanitizes a list of Gerrit hashtags."""
3505 subject = (self._description_lines or ('',))[0]
3506 subject = re.sub(
3507 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3508
3509 tags = []
3510 start = 0
3511 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3512 while True:
3513 m = bracket_exp.match(subject, start)
3514 if not m:
3515 break
3516 tags.append(self.sanitize_hash_tag(m.group(1)))
3517 start = m.end()
3518
3519 if not tags:
3520 # Try "Tag: " prefix.
3521 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3522 if m:
3523 tags.append(self.sanitize_hash_tag(m.group(1)))
3524 return tags
3525
3526 @classmethod
3527 def sanitize_hash_tag(cls, tag):
3528 """Returns a sanitized Gerrit hash tag.
3529
3530 A sanitized hashtag can be used as a git push refspec parameter value.
3531 """
3532 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3533
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003534 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3535 """Updates this commit description given the parent.
3536
3537 This is essentially what Gnumbd used to do.
3538 Consult https://goo.gl/WMmpDe for more details.
3539 """
3540 assert parent_msg # No, orphan branch creation isn't supported.
3541 assert parent_hash
3542 assert dest_ref
3543 parent_footer_map = git_footers.parse_footers(parent_msg)
3544 # This will also happily parse svn-position, which GnumbD is no longer
3545 # supporting. While we'd generate correct footers, the verifier plugin
3546 # installed in Gerrit will block such commit (ie git push below will fail).
3547 parent_position = git_footers.get_position(parent_footer_map)
3548
3549 # Cherry-picks may have last line obscuring their prior footers,
3550 # from git_footers perspective. This is also what Gnumbd did.
3551 cp_line = None
3552 if (self._description_lines and
3553 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3554 cp_line = self._description_lines.pop()
3555
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003556 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003557
3558 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3559 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003560 for i, line in enumerate(footer_lines):
3561 k, v = git_footers.parse_footer(line) or (None, None)
3562 if k and k.startswith('Cr-'):
3563 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003564
3565 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003566 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003567 if parent_position[0] == dest_ref:
3568 # Same branch as parent.
3569 number = int(parent_position[1]) + 1
3570 else:
3571 number = 1 # New branch, and extra lineage.
3572 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3573 int(parent_position[1])))
3574
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003575 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3576 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003577
3578 self._description_lines = top_lines
3579 if cp_line:
3580 self._description_lines.append(cp_line)
3581 if self._description_lines[-1] != '':
3582 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003583 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003584
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003585
Aaron Gablea1bab272017-04-11 16:38:18 -07003586def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003587 """Retrieves the reviewers that approved a CL from the issue properties with
3588 messages.
3589
3590 Note that the list may contain reviewers that are not committer, thus are not
3591 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003592
3593 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003594 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003595 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003596 return sorted(
3597 set(
3598 message['sender']
3599 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003600 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003601 )
3602 )
3603
3604
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003605def FindCodereviewSettingsFile(filename='codereview.settings'):
3606 """Finds the given file starting in the cwd and going up.
3607
3608 Only looks up to the top of the repository unless an
3609 'inherit-review-settings-ok' file exists in the root of the repository.
3610 """
3611 inherit_ok_file = 'inherit-review-settings-ok'
3612 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003613 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003614 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3615 root = '/'
3616 while True:
3617 if filename in os.listdir(cwd):
3618 if os.path.isfile(os.path.join(cwd, filename)):
3619 return open(os.path.join(cwd, filename))
3620 if cwd == root:
3621 break
3622 cwd = os.path.dirname(cwd)
3623
3624
3625def LoadCodereviewSettingsFromFile(fileobj):
3626 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003627 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003629 def SetProperty(name, setting, unset_error_ok=False):
3630 fullname = 'rietveld.' + name
3631 if setting in keyvals:
3632 RunGit(['config', fullname, keyvals[setting]])
3633 else:
3634 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3635
tandrii48df5812016-10-17 03:55:37 -07003636 if not keyvals.get('GERRIT_HOST', False):
3637 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003638 # Only server setting is required. Other settings can be absent.
3639 # In that case, we ignore errors raised during option deletion attempt.
3640 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003641 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003642 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3643 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003644 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003645 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3646 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003647 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003648 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3649 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003650
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003651 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003652 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003653
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003654 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003655 RunGit(['config', 'gerrit.squash-uploads',
3656 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003657
tandrii@chromium.org28253532016-04-14 13:46:56 +00003658 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003659 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003660 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3661
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003662 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003663 # should be of the form
3664 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3665 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003666 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3667 keyvals['ORIGIN_URL_CONFIG']])
3668
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003669
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003670def urlretrieve(source, destination):
3671 """urllib is broken for SSL connections via a proxy therefore we
3672 can't use urllib.urlretrieve()."""
3673 with open(destination, 'w') as f:
3674 f.write(urllib2.urlopen(source).read())
3675
3676
ukai@chromium.org712d6102013-11-27 00:52:58 +00003677def hasSheBang(fname):
3678 """Checks fname is a #! script."""
3679 with open(fname) as f:
3680 return f.read(2).startswith('#!')
3681
3682
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003683# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3684def DownloadHooks(*args, **kwargs):
3685 pass
3686
3687
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003688def DownloadGerritHook(force):
3689 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003690
3691 Args:
3692 force: True to update hooks. False to install hooks if not present.
3693 """
3694 if not settings.GetIsGerrit():
3695 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003696 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003697 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3698 if not os.access(dst, os.X_OK):
3699 if os.path.exists(dst):
3700 if not force:
3701 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003702 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003703 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003704 if not hasSheBang(dst):
3705 DieWithError('Not a script: %s\n'
3706 'You need to download from\n%s\n'
3707 'into .git/hooks/commit-msg and '
3708 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003709 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3710 except Exception:
3711 if os.path.exists(dst):
3712 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003713 DieWithError('\nFailed to download hooks.\n'
3714 'You need to download from\n%s\n'
3715 'into .git/hooks/commit-msg and '
3716 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003717
3718
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003719def GetRietveldCodereviewSettingsInteractively():
3720 """Prompt the user for settings."""
3721 server = settings.GetDefaultServerUrl(error_ok=True)
3722 prompt = 'Rietveld server (host[:port])'
3723 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3724 newserver = ask_for_data(prompt + ':')
3725 if not server and not newserver:
3726 newserver = DEFAULT_SERVER
3727 if newserver:
3728 newserver = gclient_utils.UpgradeToHttps(newserver)
3729 if newserver != server:
3730 RunGit(['config', 'rietveld.server', newserver])
3731
3732 def SetProperty(initial, caption, name, is_url):
3733 prompt = caption
3734 if initial:
3735 prompt += ' ("x" to clear) [%s]' % initial
3736 new_val = ask_for_data(prompt + ':')
3737 if new_val == 'x':
3738 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3739 elif new_val:
3740 if is_url:
3741 new_val = gclient_utils.UpgradeToHttps(new_val)
3742 if new_val != initial:
3743 RunGit(['config', 'rietveld.' + name, new_val])
3744
3745 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3746 SetProperty(settings.GetDefaultPrivateFlag(),
3747 'Private flag (rietveld only)', 'private', False)
3748 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3749 'tree-status-url', False)
3750 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3751 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3752 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3753 'run-post-upload-hook', False)
3754
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003755
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003756class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003757 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003758
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003759 _GOOGLESOURCE = 'googlesource.com'
3760
3761 def __init__(self):
3762 # Cached list of [host, identity, source], where source is either
3763 # .gitcookies or .netrc.
3764 self._all_hosts = None
3765
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003766 def ensure_configured_gitcookies(self):
3767 """Runs checks and suggests fixes to make git use .gitcookies from default
3768 path."""
3769 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3770 configured_path = RunGitSilent(
3771 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003772 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003773 if configured_path:
3774 self._ensure_default_gitcookies_path(configured_path, default)
3775 else:
3776 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003777
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003778 @staticmethod
3779 def _ensure_default_gitcookies_path(configured_path, default_path):
3780 assert configured_path
3781 if configured_path == default_path:
3782 print('git is already configured to use your .gitcookies from %s' %
3783 configured_path)
3784 return
3785
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003786 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003787 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3788 (configured_path, default_path))
3789
3790 if not os.path.exists(configured_path):
3791 print('However, your configured .gitcookies file is missing.')
3792 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3793 action='reconfigure')
3794 RunGit(['config', '--global', 'http.cookiefile', default_path])
3795 return
3796
3797 if os.path.exists(default_path):
3798 print('WARNING: default .gitcookies file already exists %s' %
3799 default_path)
3800 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3801 default_path)
3802
3803 confirm_or_exit('Move existing .gitcookies to default location?',
3804 action='move')
3805 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003806 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003807 print('Moved and reconfigured git to use .gitcookies from %s' %
3808 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003809
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003810 @staticmethod
3811 def _configure_gitcookies_path(default_path):
3812 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3813 if os.path.exists(netrc_path):
3814 print('You seem to be using outdated .netrc for git credentials: %s' %
3815 netrc_path)
3816 print('This tool will guide you through setting up recommended '
3817 '.gitcookies store for git credentials.\n'
3818 '\n'
3819 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3820 ' git config --global --unset http.cookiefile\n'
3821 ' mv %s %s.backup\n\n' % (default_path, default_path))
3822 confirm_or_exit(action='setup .gitcookies')
3823 RunGit(['config', '--global', 'http.cookiefile', default_path])
3824 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003825
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003826 def get_hosts_with_creds(self, include_netrc=False):
3827 if self._all_hosts is None:
3828 a = gerrit_util.CookiesAuthenticator()
3829 self._all_hosts = [
3830 (h, u, s)
3831 for h, u, s in itertools.chain(
3832 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3833 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3834 )
3835 if h.endswith(self._GOOGLESOURCE)
3836 ]
3837
3838 if include_netrc:
3839 return self._all_hosts
3840 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3841
3842 def print_current_creds(self, include_netrc=False):
3843 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3844 if not hosts:
3845 print('No Git/Gerrit credentials found')
3846 return
3847 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3848 header = [('Host', 'User', 'Which file'),
3849 ['=' * l for l in lengths]]
3850 for row in (header + hosts):
3851 print('\t'.join((('%%+%ds' % l) % s)
3852 for l, s in zip(lengths, row)))
3853
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003854 @staticmethod
3855 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003856 """Parses identity "git-<username>.domain" into <username> and domain."""
3857 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003858 # distinguishable from sub-domains. But we do know typical domains:
3859 if identity.endswith('.chromium.org'):
3860 domain = 'chromium.org'
3861 username = identity[:-len('.chromium.org')]
3862 else:
3863 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003864 if username.startswith('git-'):
3865 username = username[len('git-'):]
3866 return username, domain
3867
3868 def _get_usernames_of_domain(self, domain):
3869 """Returns list of usernames referenced by .gitcookies in a given domain."""
3870 identities_by_domain = {}
3871 for _, identity, _ in self.get_hosts_with_creds():
3872 username, domain = self._parse_identity(identity)
3873 identities_by_domain.setdefault(domain, []).append(username)
3874 return identities_by_domain.get(domain)
3875
3876 def _canonical_git_googlesource_host(self, host):
3877 """Normalizes Gerrit hosts (with '-review') to Git host."""
3878 assert host.endswith(self._GOOGLESOURCE)
3879 # Prefix doesn't include '.' at the end.
3880 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3881 if prefix.endswith('-review'):
3882 prefix = prefix[:-len('-review')]
3883 return prefix + '.' + self._GOOGLESOURCE
3884
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003885 def _canonical_gerrit_googlesource_host(self, host):
3886 git_host = self._canonical_git_googlesource_host(host)
3887 prefix = git_host.split('.', 1)[0]
3888 return prefix + '-review.' + self._GOOGLESOURCE
3889
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003890 def _get_counterpart_host(self, host):
3891 assert host.endswith(self._GOOGLESOURCE)
3892 git = self._canonical_git_googlesource_host(host)
3893 gerrit = self._canonical_gerrit_googlesource_host(git)
3894 return git if gerrit == host else gerrit
3895
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003896 def has_generic_host(self):
3897 """Returns whether generic .googlesource.com has been configured.
3898
3899 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3900 """
3901 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3902 if host == '.' + self._GOOGLESOURCE:
3903 return True
3904 return False
3905
3906 def _get_git_gerrit_identity_pairs(self):
3907 """Returns map from canonic host to pair of identities (Git, Gerrit).
3908
3909 One of identities might be None, meaning not configured.
3910 """
3911 host_to_identity_pairs = {}
3912 for host, identity, _ in self.get_hosts_with_creds():
3913 canonical = self._canonical_git_googlesource_host(host)
3914 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3915 idx = 0 if canonical == host else 1
3916 pair[idx] = identity
3917 return host_to_identity_pairs
3918
3919 def get_partially_configured_hosts(self):
3920 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003921 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3922 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3923 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003924
3925 def get_conflicting_hosts(self):
3926 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003927 host
3928 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003929 if None not in (i1, i2) and i1 != i2)
3930
3931 def get_duplicated_hosts(self):
3932 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3933 return set(host for host, count in counters.iteritems() if count > 1)
3934
3935 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3936 'chromium.googlesource.com': 'chromium.org',
3937 'chrome-internal.googlesource.com': 'google.com',
3938 }
3939
3940 def get_hosts_with_wrong_identities(self):
3941 """Finds hosts which **likely** reference wrong identities.
3942
3943 Note: skips hosts which have conflicting identities for Git and Gerrit.
3944 """
3945 hosts = set()
3946 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3947 pair = self._get_git_gerrit_identity_pairs().get(host)
3948 if pair and pair[0] == pair[1]:
3949 _, domain = self._parse_identity(pair[0])
3950 if domain != expected:
3951 hosts.add(host)
3952 return hosts
3953
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003954 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003955 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003956 hosts = sorted(hosts)
3957 assert hosts
3958 if extra_column_func is None:
3959 extras = [''] * len(hosts)
3960 else:
3961 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003962 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3963 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003964 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003965 lines.append(tmpl % he)
3966 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003967
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003968 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003969 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003970 yield ('.googlesource.com wildcard record detected',
3971 ['Chrome Infrastructure team recommends to list full host names '
3972 'explicitly.'],
3973 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003974
3975 dups = self.get_duplicated_hosts()
3976 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003977 yield ('The following hosts were defined twice',
3978 self._format_hosts(dups),
3979 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003980
3981 partial = self.get_partially_configured_hosts()
3982 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003983 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3984 'These hosts are missing',
3985 self._format_hosts(partial, lambda host: 'but %s defined' %
3986 self._get_counterpart_host(host)),
3987 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003988
3989 conflicting = self.get_conflicting_hosts()
3990 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003991 yield ('The following Git hosts have differing credentials from their '
3992 'Gerrit counterparts',
3993 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3994 tuple(self._get_git_gerrit_identity_pairs()[host])),
3995 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003996
3997 wrong = self.get_hosts_with_wrong_identities()
3998 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003999 yield ('These hosts likely use wrong identity',
4000 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4001 (self._get_git_gerrit_identity_pairs()[host][0],
4002 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4003 wrong)
4004
4005 def find_and_report_problems(self):
4006 """Returns True if there was at least one problem, else False."""
4007 found = False
4008 bad_hosts = set()
4009 for title, sublines, hosts in self._find_problems():
4010 if not found:
4011 found = True
4012 print('\n\n.gitcookies problem report:\n')
4013 bad_hosts.update(hosts or [])
4014 print(' %s%s' % (title , (':' if sublines else '')))
4015 if sublines:
4016 print()
4017 print(' %s' % '\n '.join(sublines))
4018 print()
4019
4020 if bad_hosts:
4021 assert found
4022 print(' You can manually remove corresponding lines in your %s file and '
4023 'visit the following URLs with correct account to generate '
4024 'correct credential lines:\n' %
4025 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4026 print(' %s' % '\n '.join(sorted(set(
4027 gerrit_util.CookiesAuthenticator().get_new_password_url(
4028 self._canonical_git_googlesource_host(host))
4029 for host in bad_hosts
4030 ))))
4031 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004032
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004033
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004034@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004035def CMDcreds_check(parser, args):
4036 """Checks credentials and suggests changes."""
4037 _, _ = parser.parse_args(args)
4038
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004039 # Code below checks .gitcookies. Abort if using something else.
4040 authn = gerrit_util.Authenticator.get()
4041 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
4042 if isinstance(authn, gerrit_util.GceAuthenticator):
4043 DieWithError(
4044 'This command is not designed for GCE, are you on a bot?\n'
4045 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
4046 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004047 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004048 'This command is not designed for bot environment. It checks '
4049 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004050
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004051 checker = _GitCookiesChecker()
4052 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004053
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004054 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004055 checker.print_current_creds(include_netrc=True)
4056
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004057 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004058 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004059 return 0
4060 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004061
4062
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004063@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004064@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004065def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004066 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004067
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004068 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004069 # TODO(tandrii): remove this once we switch to Gerrit.
4070 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004071 parser.add_option('--activate-update', action='store_true',
4072 help='activate auto-updating [rietveld] section in '
4073 '.git/config')
4074 parser.add_option('--deactivate-update', action='store_true',
4075 help='deactivate auto-updating [rietveld] section in '
4076 '.git/config')
4077 options, args = parser.parse_args(args)
4078
4079 if options.deactivate_update:
4080 RunGit(['config', 'rietveld.autoupdate', 'false'])
4081 return
4082
4083 if options.activate_update:
4084 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4085 return
4086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004087 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004088 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004089 return 0
4090
4091 url = args[0]
4092 if not url.endswith('codereview.settings'):
4093 url = os.path.join(url, 'codereview.settings')
4094
4095 # Load code review settings and download hooks (if available).
4096 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4097 return 0
4098
4099
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004100@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004101def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004102 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004103 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4104 branch = ShortBranchName(branchref)
4105 _, args = parser.parse_args(args)
4106 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004107 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004108 return RunGit(['config', 'branch.%s.base-url' % branch],
4109 error_ok=False).strip()
4110 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004111 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004112 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4113 error_ok=False).strip()
4114
4115
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004116def color_for_status(status):
4117 """Maps a Changelist status to color, for CMDstatus and other tools."""
4118 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004119 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004120 'waiting': Fore.BLUE,
4121 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004122 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004123 'lgtm': Fore.GREEN,
4124 'commit': Fore.MAGENTA,
4125 'closed': Fore.CYAN,
4126 'error': Fore.WHITE,
4127 }.get(status, Fore.WHITE)
4128
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004129
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004130def get_cl_statuses(changes, fine_grained, max_processes=None):
4131 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004132
4133 If fine_grained is true, this will fetch CL statuses from the server.
4134 Otherwise, simply indicate if there's a matching url for the given branches.
4135
4136 If max_processes is specified, it is used as the maximum number of processes
4137 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4138 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004139
4140 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004141 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004142 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004143 upload.verbosity = 0
4144
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004145 if not changes:
4146 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004147
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004148 if not fine_grained:
4149 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004150 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004151 for cl in changes:
4152 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004153 return
4154
4155 # First, sort out authentication issues.
4156 logging.debug('ensuring credentials exist')
4157 for cl in changes:
4158 cl.EnsureAuthenticated(force=False, refresh=True)
4159
4160 def fetch(cl):
4161 try:
4162 return (cl, cl.GetStatus())
4163 except:
4164 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004165 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004166 raise
4167
4168 threads_count = len(changes)
4169 if max_processes:
4170 threads_count = max(1, min(threads_count, max_processes))
4171 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4172
4173 pool = ThreadPool(threads_count)
4174 fetched_cls = set()
4175 try:
4176 it = pool.imap_unordered(fetch, changes).__iter__()
4177 while True:
4178 try:
4179 cl, status = it.next(timeout=5)
4180 except multiprocessing.TimeoutError:
4181 break
4182 fetched_cls.add(cl)
4183 yield cl, status
4184 finally:
4185 pool.close()
4186
4187 # Add any branches that failed to fetch.
4188 for cl in set(changes) - fetched_cls:
4189 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004190
rmistry@google.com2dd99862015-06-22 12:22:18 +00004191
4192def upload_branch_deps(cl, args):
4193 """Uploads CLs of local branches that are dependents of the current branch.
4194
4195 If the local branch dependency tree looks like:
4196 test1 -> test2.1 -> test3.1
4197 -> test3.2
4198 -> test2.2 -> test3.3
4199
4200 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4201 run on the dependent branches in this order:
4202 test2.1, test3.1, test3.2, test2.2, test3.3
4203
4204 Note: This function does not rebase your local dependent branches. Use it when
4205 you make a change to the parent branch that will not conflict with its
4206 dependent branches, and you would like their dependencies updated in
4207 Rietveld.
4208 """
4209 if git_common.is_dirty_git_tree('upload-branch-deps'):
4210 return 1
4211
4212 root_branch = cl.GetBranch()
4213 if root_branch is None:
4214 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4215 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004216 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00004217 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4218 'patchset dependencies without an uploaded CL.')
4219
4220 branches = RunGit(['for-each-ref',
4221 '--format=%(refname:short) %(upstream:short)',
4222 'refs/heads'])
4223 if not branches:
4224 print('No local branches found.')
4225 return 0
4226
4227 # Create a dictionary of all local branches to the branches that are dependent
4228 # on it.
4229 tracked_to_dependents = collections.defaultdict(list)
4230 for b in branches.splitlines():
4231 tokens = b.split()
4232 if len(tokens) == 2:
4233 branch_name, tracked = tokens
4234 tracked_to_dependents[tracked].append(branch_name)
4235
vapiera7fbd5a2016-06-16 09:17:49 -07004236 print()
4237 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004238 dependents = []
4239 def traverse_dependents_preorder(branch, padding=''):
4240 dependents_to_process = tracked_to_dependents.get(branch, [])
4241 padding += ' '
4242 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004243 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004244 dependents.append(dependent)
4245 traverse_dependents_preorder(dependent, padding)
4246 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004247 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004248
4249 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004250 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004251 return 0
4252
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004253 confirm_or_exit('This command will checkout all dependent branches and run '
4254 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004255
rmistry@google.com2dd99862015-06-22 12:22:18 +00004256 # Record all dependents that failed to upload.
4257 failures = {}
4258 # Go through all dependents, checkout the branch and upload.
4259 try:
4260 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004261 print()
4262 print('--------------------------------------')
4263 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004264 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004265 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004266 try:
4267 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004268 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004269 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004270 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004271 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004272 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004273 finally:
4274 # Swap back to the original root branch.
4275 RunGit(['checkout', '-q', root_branch])
4276
vapiera7fbd5a2016-06-16 09:17:49 -07004277 print()
4278 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004279 for dependent_branch in dependents:
4280 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004281 print(' %s : %s' % (dependent_branch, upload_status))
4282 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004283
4284 return 0
4285
4286
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004287@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004288def CMDarchive(parser, args):
4289 """Archives and deletes branches associated with closed changelists."""
4290 parser.add_option(
4291 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004292 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004293 parser.add_option(
4294 '-f', '--force', action='store_true',
4295 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004296 parser.add_option(
4297 '-d', '--dry-run', action='store_true',
4298 help='Skip the branch tagging and removal steps.')
4299 parser.add_option(
4300 '-t', '--notags', action='store_true',
4301 help='Do not tag archived branches. '
4302 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004303
4304 auth.add_auth_options(parser)
4305 options, args = parser.parse_args(args)
4306 if args:
4307 parser.error('Unsupported args: %s' % ' '.join(args))
4308 auth_config = auth.extract_auth_config_from_options(options)
4309
4310 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4311 if not branches:
4312 return 0
4313
vapiera7fbd5a2016-06-16 09:17:49 -07004314 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004315 changes = [Changelist(branchref=b, auth_config=auth_config)
4316 for b in branches.splitlines()]
4317 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4318 statuses = get_cl_statuses(changes,
4319 fine_grained=True,
4320 max_processes=options.maxjobs)
4321 proposal = [(cl.GetBranch(),
4322 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4323 for cl, status in statuses
4324 if status == 'closed']
4325 proposal.sort()
4326
4327 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004328 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004329 return 0
4330
4331 current_branch = GetCurrentBranch()
4332
vapiera7fbd5a2016-06-16 09:17:49 -07004333 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004334 if options.notags:
4335 for next_item in proposal:
4336 print(' ' + next_item[0])
4337 else:
4338 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4339 for next_item in proposal:
4340 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004341
kmarshall9249e012016-08-23 12:02:16 -07004342 # Quit now on precondition failure or if instructed by the user, either
4343 # via an interactive prompt or by command line flags.
4344 if options.dry_run:
4345 print('\nNo changes were made (dry run).\n')
4346 return 0
4347 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004348 print('You are currently on a branch \'%s\' which is associated with a '
4349 'closed codereview issue, so archive cannot proceed. Please '
4350 'checkout another branch and run this command again.' %
4351 current_branch)
4352 return 1
kmarshall9249e012016-08-23 12:02:16 -07004353 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004354 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4355 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004356 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004357 return 1
4358
4359 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004360 if not options.notags:
4361 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004362 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004363
vapiera7fbd5a2016-06-16 09:17:49 -07004364 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004365
4366 return 0
4367
4368
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004369@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004370def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004371 """Show status of changelists.
4372
4373 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004374 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004375 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004376 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004377 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004378 - Magenta in the commit queue
4379 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004380 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004381
4382 Also see 'git cl comments'.
4383 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004384 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004385 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004386 parser.add_option('-f', '--fast', action='store_true',
4387 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004388 parser.add_option(
4389 '-j', '--maxjobs', action='store', type=int,
4390 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004391
4392 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004393 _add_codereview_issue_select_options(
4394 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004395 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004396 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004397 if args:
4398 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004399 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004400
iannuccie53c9352016-08-17 14:40:40 -07004401 if options.issue is not None and not options.field:
4402 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004403
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004404 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004405 cl = Changelist(auth_config=auth_config, issue=options.issue,
4406 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004407 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004408 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004409 elif options.field == 'id':
4410 issueid = cl.GetIssue()
4411 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004412 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004413 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004414 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004415 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004416 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004417 elif options.field == 'status':
4418 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004419 elif options.field == 'url':
4420 url = cl.GetIssueURL()
4421 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004422 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004423 return 0
4424
4425 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4426 if not branches:
4427 print('No local branch found.')
4428 return 0
4429
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004430 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004431 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004432 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004433 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004434 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004435 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004436 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004437
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004438 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004439 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4440 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4441 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004442 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004443 c, status = output.next()
4444 branch_statuses[c.GetBranch()] = status
4445 status = branch_statuses.pop(branch)
4446 url = cl.GetIssueURL()
4447 if url and (not status or status == 'error'):
4448 # The issue probably doesn't exist anymore.
4449 url += ' (broken)'
4450
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004451 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004452 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004453 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004454 color = ''
4455 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004456 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004457 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004458 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004459 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004460
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004461
4462 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004463 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004464 print('Current branch: %s' % branch)
4465 for cl in changes:
4466 if cl.GetBranch() == branch:
4467 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004468 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004469 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004470 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004471 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004472 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004473 print('Issue description:')
4474 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004475 return 0
4476
4477
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004478def colorize_CMDstatus_doc():
4479 """To be called once in main() to add colors to git cl status help."""
4480 colors = [i for i in dir(Fore) if i[0].isupper()]
4481
4482 def colorize_line(line):
4483 for color in colors:
4484 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004485 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004486 indent = len(line) - len(line.lstrip(' ')) + 1
4487 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4488 return line
4489
4490 lines = CMDstatus.__doc__.splitlines()
4491 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4492
4493
phajdan.jre328cf92016-08-22 04:12:17 -07004494def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004495 if path == '-':
4496 json.dump(contents, sys.stdout)
4497 else:
4498 with open(path, 'w') as f:
4499 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004500
4501
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004502@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004503@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004504def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004505 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004506
4507 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004508 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004509 parser.add_option('-r', '--reverse', action='store_true',
4510 help='Lookup the branch(es) for the specified issues. If '
4511 'no issues are specified, all branches with mapped '
4512 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004513 parser.add_option('--json',
4514 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004515 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004516 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004517 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004518
dnj@chromium.org406c4402015-03-03 17:22:28 +00004519 if options.reverse:
4520 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004521 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004522 # Reverse issue lookup.
4523 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004524
4525 git_config = {}
4526 for config in RunGit(['config', '--get-regexp',
4527 r'branch\..*issue']).splitlines():
4528 name, _space, val = config.partition(' ')
4529 git_config[name] = val
4530
dnj@chromium.org406c4402015-03-03 17:22:28 +00004531 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004532 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4533 config_key = _git_branch_config_key(ShortBranchName(branch),
4534 cls.IssueConfigKey())
4535 issue = git_config.get(config_key)
4536 if issue:
4537 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004538 if not args:
4539 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004540 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004541 for issue in args:
4542 if not issue:
4543 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004544 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004545 print('Branch for issue number %s: %s' % (
4546 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004547 if options.json:
4548 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004549 return 0
4550
4551 if len(args) > 0:
4552 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4553 if not issue.valid:
4554 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4555 'or no argument to list it.\n'
4556 'Maybe you want to run git cl status?')
4557 cl = Changelist(codereview=issue.codereview)
4558 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004559 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004560 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004561 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4562 if options.json:
4563 write_json(options.json, {
4564 'issue': cl.GetIssue(),
4565 'issue_url': cl.GetIssueURL(),
4566 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004567 return 0
4568
4569
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004570@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004571def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004572 """Shows or posts review comments for any changelist."""
4573 parser.add_option('-a', '--add-comment', dest='comment',
4574 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004575 parser.add_option('-i', '--issue', dest='issue',
4576 help='review issue id (defaults to current issue). '
4577 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004578 parser.add_option('-m', '--machine-readable', dest='readable',
4579 action='store_false', default=True,
4580 help='output comments in a format compatible with '
4581 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004582 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004583 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004584 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004585 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004586 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004587 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004588 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004589
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004590 issue = None
4591 if options.issue:
4592 try:
4593 issue = int(options.issue)
4594 except ValueError:
4595 DieWithError('A review issue id is expected to be a number')
4596
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004597 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4598
4599 if not cl.IsGerrit():
4600 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004601
4602 if options.comment:
4603 cl.AddComment(options.comment)
4604 return 0
4605
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004606 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4607 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004608 for comment in summary:
4609 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004610 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004611 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004612 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004613 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004614 color = Fore.MAGENTA
4615 else:
4616 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004617 print('\n%s%s %s%s\n%s' % (
4618 color,
4619 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4620 comment.sender,
4621 Fore.RESET,
4622 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4623
smut@google.comc85ac942015-09-15 16:34:43 +00004624 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004625 def pre_serialize(c):
4626 dct = c.__dict__.copy()
4627 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4628 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004629 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004630 return 0
4631
4632
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004633@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004634@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004635def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004636 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004637 parser.add_option('-d', '--display', action='store_true',
4638 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004639 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004640 help='New description to set for this issue (- for stdin, '
4641 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004642 parser.add_option('-f', '--force', action='store_true',
4643 help='Delete any unpublished Gerrit edits for this issue '
4644 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004645
4646 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004647 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004648 options, args = parser.parse_args(args)
4649 _process_codereview_select_options(parser, options)
4650
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004651 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004652 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004653 target_issue_arg = ParseIssueNumberArgument(args[0],
4654 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004655 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004656 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004657
martiniss6eda05f2016-06-30 10:18:35 -07004658 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004659 'auth_config': auth.extract_auth_config_from_options(options),
4660 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004661 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004662 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004663 if target_issue_arg:
4664 kwargs['issue'] = target_issue_arg.issue
4665 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004666 if target_issue_arg.codereview and not options.forced_codereview:
4667 detected_codereview_from_url = True
4668 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004669
4670 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004671 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004672 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004673 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004674
4675 if detected_codereview_from_url:
4676 logging.info('canonical issue/change URL: %s (type: %s)\n',
4677 cl.GetIssueURL(), target_issue_arg.codereview)
4678
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004679 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004680
smut@google.com34fb6b12015-07-13 20:03:26 +00004681 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004682 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004683 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004684
4685 if options.new_description:
4686 text = options.new_description
4687 if text == '-':
4688 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004689 elif text == '+':
4690 base_branch = cl.GetCommonAncestorWithUpstream()
4691 change = cl.GetChange(base_branch, None, local_description=True)
4692 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004693
4694 description.set_description(text)
4695 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004696 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004697
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004698 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004699 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004700 return 0
4701
4702
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004703@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004704def CMDlint(parser, args):
4705 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004706 parser.add_option('--filter', action='append', metavar='-x,+y',
4707 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004708 auth.add_auth_options(parser)
4709 options, args = parser.parse_args(args)
4710 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004711
4712 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004713 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004714 try:
4715 import cpplint
4716 import cpplint_chromium
4717 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004718 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004719 return 1
4720
4721 # Change the current working directory before calling lint so that it
4722 # shows the correct base.
4723 previous_cwd = os.getcwd()
4724 os.chdir(settings.GetRoot())
4725 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004726 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004727 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4728 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004729 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004730 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004731 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004732
4733 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004734 command = args + files
4735 if options.filter:
4736 command = ['--filter=' + ','.join(options.filter)] + command
4737 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004738
4739 white_regex = re.compile(settings.GetLintRegex())
4740 black_regex = re.compile(settings.GetLintIgnoreRegex())
4741 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4742 for filename in filenames:
4743 if white_regex.match(filename):
4744 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004745 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004746 else:
4747 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4748 extra_check_functions)
4749 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004750 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004751 finally:
4752 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004753 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004754 if cpplint._cpplint_state.error_count != 0:
4755 return 1
4756 return 0
4757
4758
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004759@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004760def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004761 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004762 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004763 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004764 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004765 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004766 parser.add_option('--all', action='store_true',
4767 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004768 parser.add_option('--parallel', action='store_true',
4769 help='Run all tests specified by input_api.RunTests in all '
4770 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004771 auth.add_auth_options(parser)
4772 options, args = parser.parse_args(args)
4773 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004774
sbc@chromium.org71437c02015-04-09 19:29:40 +00004775 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004776 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004777 return 1
4778
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004779 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004780 if args:
4781 base_branch = args[0]
4782 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004783 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004784 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004785
Aaron Gable8076c282017-11-29 14:39:41 -08004786 if options.all:
4787 base_change = cl.GetChange(base_branch, None)
4788 files = [('M', f) for f in base_change.AllFiles()]
4789 change = presubmit_support.GitChange(
4790 base_change.Name(),
4791 base_change.FullDescriptionText(),
4792 base_change.RepositoryRoot(),
4793 files,
4794 base_change.issue,
4795 base_change.patchset,
4796 base_change.author_email,
4797 base_change._upstream)
4798 else:
4799 change = cl.GetChange(base_branch, None)
4800
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004801 cl.RunHook(
4802 committing=not options.upload,
4803 may_prompt=False,
4804 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004805 change=change,
4806 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004807 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004808
4809
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004810def GenerateGerritChangeId(message):
4811 """Returns Ixxxxxx...xxx change id.
4812
4813 Works the same way as
4814 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4815 but can be called on demand on all platforms.
4816
4817 The basic idea is to generate git hash of a state of the tree, original commit
4818 message, author/committer info and timestamps.
4819 """
4820 lines = []
4821 tree_hash = RunGitSilent(['write-tree'])
4822 lines.append('tree %s' % tree_hash.strip())
4823 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4824 if code == 0:
4825 lines.append('parent %s' % parent.strip())
4826 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4827 lines.append('author %s' % author.strip())
4828 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4829 lines.append('committer %s' % committer.strip())
4830 lines.append('')
4831 # Note: Gerrit's commit-hook actually cleans message of some lines and
4832 # whitespace. This code is not doing this, but it clearly won't decrease
4833 # entropy.
4834 lines.append(message)
4835 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4836 stdin='\n'.join(lines))
4837 return 'I%s' % change_hash.strip()
4838
4839
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004840def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004841 """Computes the remote branch ref to use for the CL.
4842
4843 Args:
4844 remote (str): The git remote for the CL.
4845 remote_branch (str): The git remote branch for the CL.
4846 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004847 """
4848 if not (remote and remote_branch):
4849 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004850
wittman@chromium.org455dc922015-01-26 20:15:50 +00004851 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004852 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004853 # refs, which are then translated into the remote full symbolic refs
4854 # below.
4855 if '/' not in target_branch:
4856 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4857 else:
4858 prefix_replacements = (
4859 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4860 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4861 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4862 )
4863 match = None
4864 for regex, replacement in prefix_replacements:
4865 match = re.search(regex, target_branch)
4866 if match:
4867 remote_branch = target_branch.replace(match.group(0), replacement)
4868 break
4869 if not match:
4870 # This is a branch path but not one we recognize; use as-is.
4871 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004872 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4873 # Handle the refs that need to land in different refs.
4874 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004875
wittman@chromium.org455dc922015-01-26 20:15:50 +00004876 # Create the true path to the remote branch.
4877 # Does the following translation:
4878 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4879 # * refs/remotes/origin/master -> refs/heads/master
4880 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4881 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4882 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4883 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4884 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4885 'refs/heads/')
4886 elif remote_branch.startswith('refs/remotes/branch-heads'):
4887 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004888
wittman@chromium.org455dc922015-01-26 20:15:50 +00004889 return remote_branch
4890
4891
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004892def cleanup_list(l):
4893 """Fixes a list so that comma separated items are put as individual items.
4894
4895 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4896 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4897 """
4898 items = sum((i.split(',') for i in l), [])
4899 stripped_items = (i.strip() for i in items)
4900 return sorted(filter(None, stripped_items))
4901
4902
Aaron Gable4db38df2017-11-03 14:59:07 -07004903@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004904@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004905def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004906 """Uploads the current changelist to codereview.
4907
4908 Can skip dependency patchset uploads for a branch by running:
4909 git config branch.branch_name.skip-deps-uploads True
4910 To unset run:
4911 git config --unset branch.branch_name.skip-deps-uploads
4912 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004913
4914 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4915 a bug number, this bug number is automatically populated in the CL
4916 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004917
4918 If subject contains text in square brackets or has "<text>: " prefix, such
4919 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4920 [git-cl] add support for hashtags
4921 Foo bar: implement foo
4922 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004923 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004924 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4925 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004926 parser.add_option('--bypass-watchlists', action='store_true',
4927 dest='bypass_watchlists',
4928 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004929 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004930 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004931 parser.add_option('--message', '-m', dest='message',
4932 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004933 parser.add_option('-b', '--bug',
4934 help='pre-populate the bug number(s) for this issue. '
4935 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004936 parser.add_option('--message-file', dest='message_file',
4937 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004938 parser.add_option('--title', '-t', dest='title',
4939 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004940 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004941 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004942 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004943 parser.add_option('--tbrs',
4944 action='append', default=[],
4945 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004946 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004947 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004948 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004949 parser.add_option('--hashtag', dest='hashtags',
4950 action='append', default=[],
4951 help=('Gerrit hashtag for new CL; '
4952 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004953 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004954 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004955 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004956 help='tell the commit queue to commit this patchset; '
4957 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004958 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004959 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004960 metavar='TARGET',
4961 help='Apply CL to remote ref TARGET. ' +
4962 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004963 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004964 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004965 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004966 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004967 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004968 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004969 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4970 const='TBR', help='add a set of OWNERS to TBR')
4971 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4972 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004973 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4974 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004975 help='Send the patchset to do a CQ dry run right after '
4976 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004977 parser.add_option('--dependencies', action='store_true',
4978 help='Uploads CLs of all the local branches that depend on '
4979 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004980 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4981 help='Sends your change to the CQ after an approval. Only '
4982 'works on repos that have the Auto-Submit label '
4983 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004984 parser.add_option('--parallel', action='store_true',
4985 help='Run all tests specified by input_api.RunTests in all '
4986 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004987
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004988 parser.add_option('--no-autocc', action='store_true',
4989 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004990 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004991 help='Set the review private. This implies --no-autocc.')
4992
rmistry@google.com2dd99862015-06-22 12:22:18 +00004993 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004994 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004995 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004996 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004997 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004998 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004999
sbc@chromium.org71437c02015-04-09 19:29:40 +00005000 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005001 return 1
5002
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005003 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005004 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005005 options.cc = cleanup_list(options.cc)
5006
tandriib80458a2016-06-23 12:20:07 -07005007 if options.message_file:
5008 if options.message:
5009 parser.error('only one of --message and --message-file allowed.')
5010 options.message = gclient_utils.FileRead(options.message_file)
5011 options.message_file = None
5012
tandrii4d0545a2016-07-06 03:56:49 -07005013 if options.cq_dry_run and options.use_commit_queue:
5014 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5015
Aaron Gableedbc4132017-09-11 13:22:28 -07005016 if options.use_commit_queue:
5017 options.send_mail = True
5018
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005019 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5020 settings.GetIsGerrit()
5021
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005022 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00005023 if not cl.IsGerrit():
5024 # Error out with instructions for repos not yet configured for Gerrit.
5025 print('=====================================')
5026 print('NOTICE: Rietveld is no longer supported. '
5027 'You can upload changes to Gerrit with')
5028 print(' git cl upload --gerrit')
5029 print('or set Gerrit to be your default code review tool with')
5030 print(' git config gerrit.host true')
5031 print('=====================================')
5032 return 1
5033
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005034 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005035
5036
Francois Dorayd42c6812017-05-30 15:10:20 -04005037@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005038@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005039def CMDsplit(parser, args):
5040 """Splits a branch into smaller branches and uploads CLs.
5041
5042 Creates a branch and uploads a CL for each group of files modified in the
5043 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005044 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005045 the shared OWNERS file.
5046 """
5047 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005048 help="A text file containing a CL description in which "
5049 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005050 parser.add_option("-c", "--comment", dest="comment_file",
5051 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005052 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5053 default=False,
5054 help="List the files and reviewers for each CL that would "
5055 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00005056 parser.add_option("--cq-dry-run", action='store_true',
5057 help="If set, will do a cq dry run for each uploaded CL. "
5058 "Please be careful when doing this; more than ~10 CLs "
5059 "has the potential to overload our build "
5060 "infrastructure. Try to upload these not during high "
5061 "load times (usually 11-3 Mountain View time). Email "
5062 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005063 options, _ = parser.parse_args(args)
5064
5065 if not options.description_file:
5066 parser.error('No --description flag specified.')
5067
5068 def WrappedCMDupload(args):
5069 return CMDupload(OptionParser(), args)
5070
5071 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00005072 Changelist, WrappedCMDupload, options.dry_run,
5073 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005074
5075
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005076@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005077@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005078def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005079 """DEPRECATED: Used to commit the current changelist via git-svn."""
5080 message = ('git-cl no longer supports committing to SVN repositories via '
5081 'git-svn. You probably want to use `git cl land` instead.')
5082 print(message)
5083 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005084
5085
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005086# Two special branches used by git cl land.
5087MERGE_BRANCH = 'git-cl-commit'
5088CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5089
5090
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005091@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005092@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005093def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005094 """Commits the current changelist via git.
5095
5096 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5097 upstream and closes the issue automatically and atomically.
5098
5099 Otherwise (in case of Rietveld):
5100 Squashes branch into a single commit.
5101 Updates commit message with metadata (e.g. pointer to review).
5102 Pushes the code upstream.
5103 Updates review and closes.
5104 """
5105 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5106 help='bypass upload presubmit hook')
5107 parser.add_option('-m', dest='message',
5108 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005109 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005110 help="force yes to questions (don't prompt)")
5111 parser.add_option('-c', dest='contributor',
5112 help="external contributor for patch (appended to " +
5113 "description and used as author for git). Should be " +
5114 "formatted as 'First Last <email@example.com>'")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005115 parser.add_option('--parallel', action='store_true',
5116 help='Run all tests specified by input_api.RunTests in all '
5117 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005118 auth.add_auth_options(parser)
5119 (options, args) = parser.parse_args(args)
5120 auth_config = auth.extract_auth_config_from_options(options)
5121
5122 cl = Changelist(auth_config=auth_config)
5123
Robert Iannucci2e73d432018-03-14 01:10:47 -07005124 if not cl.IsGerrit():
5125 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005126
Robert Iannucci2e73d432018-03-14 01:10:47 -07005127 if options.message:
5128 # This could be implemented, but it requires sending a new patch to
5129 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5130 # Besides, Gerrit has the ability to change the commit message on submit
5131 # automatically, thus there is no need to support this option (so far?).
5132 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005133 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005134 parser.error(
5135 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5136 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5137 'the contributor\'s "name <email>". If you can\'t upload such a '
5138 'commit for review, contact your repository admin and request'
5139 '"Forge-Author" permission.')
5140 if not cl.GetIssue():
5141 DieWithError('You must upload the change first to Gerrit.\n'
5142 ' If you would rather have `git cl land` upload '
5143 'automatically for you, see http://crbug.com/642759')
5144 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005145 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005146
5147
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005148def PushToGitWithAutoRebase(remote, branch, original_description,
5149 git_numberer_enabled, max_attempts=3):
5150 """Pushes current HEAD commit on top of remote's branch.
5151
5152 Attempts to fetch and autorebase on push failures.
5153 Adds git number footers on the fly.
5154
5155 Returns integer code from last command.
5156 """
5157 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5158 code = 0
5159 attempts_left = max_attempts
5160 while attempts_left:
5161 attempts_left -= 1
5162 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5163
5164 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5165 # If fetch fails, retry.
5166 print('Fetching %s/%s...' % (remote, branch))
5167 code, out = RunGitWithCode(
5168 ['retry', 'fetch', remote,
5169 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5170 if code:
5171 print('Fetch failed with exit code %d.' % code)
5172 print(out.strip())
5173 continue
5174
5175 print('Cherry-picking commit on top of latest %s' % branch)
5176 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5177 suppress_stderr=True)
5178 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5179 code, out = RunGitWithCode(['cherry-pick', cherry])
5180 if code:
5181 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5182 'the following files have merge conflicts:' %
5183 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005184 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5185 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005186 print('Please rebase your patch and try again.')
5187 RunGitWithCode(['cherry-pick', '--abort'])
5188 break
5189
5190 commit_desc = ChangeDescription(original_description)
5191 if git_numberer_enabled:
5192 logging.debug('Adding git number footers')
5193 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5194 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5195 branch)
5196 # Ensure timestamps are monotonically increasing.
5197 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5198 _get_committer_timestamp('HEAD'))
5199 _git_amend_head(commit_desc.description, timestamp)
5200
5201 code, out = RunGitWithCode(
5202 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5203 print(out)
5204 if code == 0:
5205 break
5206 if IsFatalPushFailure(out):
5207 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005208 'user.email are correct and you have push access to the repo.\n'
5209 'Hint: run command below to diangose common Git/Gerrit credential '
5210 'problems:\n'
5211 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005212 break
5213 return code
5214
5215
5216def IsFatalPushFailure(push_stdout):
5217 """True if retrying push won't help."""
5218 return '(prohibited by Gerrit)' in push_stdout
5219
5220
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005221@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005222@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005223def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005224 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005225 parser.add_option('-b', dest='newbranch',
5226 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005227 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005228 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005229 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005230 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005231 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005232 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005233 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005234 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005235 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005236 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005237
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005238
5239 group = optparse.OptionGroup(
5240 parser,
5241 'Options for continuing work on the current issue uploaded from a '
5242 'different clone (e.g. different machine). Must be used independently '
5243 'from the other options. No issue number should be specified, and the '
5244 'branch must have an issue number associated with it')
5245 group.add_option('--reapply', action='store_true', dest='reapply',
5246 help='Reset the branch and reapply the issue.\n'
5247 'CAUTION: This will undo any local changes in this '
5248 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005249
5250 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005251 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005252 parser.add_option_group(group)
5253
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005254 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005255 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005256 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005257 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005258 auth_config = auth.extract_auth_config_from_options(options)
5259
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005260 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005261 if options.newbranch:
5262 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005263 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005264 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005265
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005266 cl = Changelist(auth_config=auth_config,
5267 codereview=options.forced_codereview)
5268 if not cl.GetIssue():
5269 parser.error('current branch must have an associated issue')
5270
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005271 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005272 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005273 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005274
5275 RunGit(['reset', '--hard', upstream])
5276 if options.pull:
5277 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005278
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005279 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5280 options.directory)
5281
5282 if len(args) != 1 or not args[0]:
5283 parser.error('Must specify issue number or url')
5284
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005285 target_issue_arg = ParseIssueNumberArgument(args[0],
5286 options.forced_codereview)
5287 if not target_issue_arg.valid:
5288 parser.error('invalid codereview url or CL id')
5289
5290 cl_kwargs = {
5291 'auth_config': auth_config,
5292 'codereview_host': target_issue_arg.hostname,
5293 'codereview': options.forced_codereview,
5294 }
5295 detected_codereview_from_url = False
5296 if target_issue_arg.codereview and not options.forced_codereview:
5297 detected_codereview_from_url = True
5298 cl_kwargs['codereview'] = target_issue_arg.codereview
5299 cl_kwargs['issue'] = target_issue_arg.issue
5300
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005301 # We don't want uncommitted changes mixed up with the patch.
5302 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005303 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005304
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005305 if options.newbranch:
5306 if options.force:
5307 RunGit(['branch', '-D', options.newbranch],
5308 stderr=subprocess2.PIPE, error_ok=True)
5309 RunGit(['new-branch', options.newbranch])
5310
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005311 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005312
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005313 if cl.IsGerrit():
5314 if options.reject:
5315 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005316 if options.directory:
5317 parser.error('--directory is not supported with Gerrit codereview.')
5318
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005319 if detected_codereview_from_url:
5320 print('canonical issue/change URL: %s (type: %s)\n' %
5321 (cl.GetIssueURL(), target_issue_arg.codereview))
5322
5323 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005324 options.nocommit, options.directory,
5325 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005326
5327
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005328def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005329 """Fetches the tree status and returns either 'open', 'closed',
5330 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005331 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005332 if url:
5333 status = urllib2.urlopen(url).read().lower()
5334 if status.find('closed') != -1 or status == '0':
5335 return 'closed'
5336 elif status.find('open') != -1 or status == '1':
5337 return 'open'
5338 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005339 return 'unset'
5340
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005341
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005342def GetTreeStatusReason():
5343 """Fetches the tree status from a json url and returns the message
5344 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005345 url = settings.GetTreeStatusUrl()
5346 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005347 connection = urllib2.urlopen(json_url)
5348 status = json.loads(connection.read())
5349 connection.close()
5350 return status['message']
5351
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005352
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005353@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005354def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005355 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005356 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005357 status = GetTreeStatus()
5358 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005359 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005360 return 2
5361
vapiera7fbd5a2016-06-16 09:17:49 -07005362 print('The tree is %s' % status)
5363 print()
5364 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005365 if status != 'open':
5366 return 1
5367 return 0
5368
5369
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005370@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005371def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005372 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005373 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005374 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005375 '-b', '--bot', action='append',
5376 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5377 'times to specify multiple builders. ex: '
5378 '"-b win_rel -b win_layout". See '
5379 'the try server waterfall for the builders name and the tests '
5380 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005381 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005382 '-B', '--bucket', default='',
5383 help=('Buildbucket bucket to send the try requests.'))
5384 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005385 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005386 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005387 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005388 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005389 help='Revision to use for the try job; default: the revision will '
5390 'be determined by the try recipe that builder runs, which usually '
5391 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005392 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005393 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005394 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005395 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005396 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005397 '--category', default='git_cl_try', help='Specify custom build category.')
5398 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005399 '--project',
5400 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005401 'in recipe to determine to which repository or directory to '
5402 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005403 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005404 '-p', '--property', dest='properties', action='append', default=[],
5405 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005406 'key2=value2 etc. The value will be treated as '
5407 'json if decodable, or as string otherwise. '
5408 'NOTE: using this may make your try job not usable for CQ, '
5409 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005410 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005411 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5412 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005413 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005414 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005415 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005416 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005417 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005418 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005419
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005420 if options.master and options.master.startswith('luci.'):
5421 parser.error(
5422 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005423 # Make sure that all properties are prop=value pairs.
5424 bad_params = [x for x in options.properties if '=' not in x]
5425 if bad_params:
5426 parser.error('Got properties with missing "=": %s' % bad_params)
5427
maruel@chromium.org15192402012-09-06 12:38:29 +00005428 if args:
5429 parser.error('Unknown arguments: %s' % args)
5430
Koji Ishii31c14782018-01-08 17:17:33 +09005431 cl = Changelist(auth_config=auth_config, issue=options.issue,
5432 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005433 if not cl.GetIssue():
5434 parser.error('Need to upload first')
5435
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005436 if cl.IsGerrit():
5437 # HACK: warm up Gerrit change detail cache to save on RPCs.
5438 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5439
tandriie113dfd2016-10-11 10:20:12 -07005440 error_message = cl.CannotTriggerTryJobReason()
5441 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005442 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005443
borenet6c0efe62016-10-19 08:13:29 -07005444 if options.bucket and options.master:
5445 parser.error('Only one of --bucket and --master may be used.')
5446
qyearsley1fdfcb62016-10-24 13:22:03 -07005447 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005448
qyearsleydd49f942016-10-28 11:57:22 -07005449 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5450 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005451 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005452 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005453 print('git cl try with no bots now defaults to CQ dry run.')
5454 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5455 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005456
borenet6c0efe62016-10-19 08:13:29 -07005457 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005458 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005459 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005460 'of bot requires an initial job from a parent (usually a builder). '
5461 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005462 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005463 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005464
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005465 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005466 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005467 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005468 except BuildbucketResponseException as ex:
5469 print('ERROR: %s' % ex)
5470 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005471 return 0
5472
5473
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005474@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005475def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005476 """Prints info about try jobs associated with current CL."""
5477 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005478 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005479 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005480 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005481 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005482 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005483 '--color', action='store_true', default=setup_color.IS_TTY,
5484 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005485 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005486 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5487 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005488 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005489 '--json', help=('Path of JSON output file to write try job results to,'
5490 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005491 parser.add_option_group(group)
5492 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005493 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005494 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005495 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005496 if args:
5497 parser.error('Unrecognized args: %s' % ' '.join(args))
5498
5499 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005500 cl = Changelist(
5501 issue=options.issue, codereview=options.forced_codereview,
5502 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005503 if not cl.GetIssue():
5504 parser.error('Need to upload first')
5505
tandrii221ab252016-10-06 08:12:04 -07005506 patchset = options.patchset
5507 if not patchset:
5508 patchset = cl.GetMostRecentPatchset()
5509 if not patchset:
5510 parser.error('Codereview doesn\'t know about issue %s. '
5511 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005512 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005513 cl.GetIssue())
5514
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005515 try:
tandrii221ab252016-10-06 08:12:04 -07005516 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005517 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005518 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005519 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005520 if options.json:
5521 write_try_results_json(options.json, jobs)
5522 else:
5523 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005524 return 0
5525
5526
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005527@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005528@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005529def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005530 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005531 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005532 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005533 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005534
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005535 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005536 if args:
5537 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005538 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005539 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005540 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005541 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005542
5543 # Clear configured merge-base, if there is one.
5544 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005545 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005546 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005547 return 0
5548
5549
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005550@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005551def CMDweb(parser, args):
5552 """Opens the current CL in the web browser."""
5553 _, args = parser.parse_args(args)
5554 if args:
5555 parser.error('Unrecognized args: %s' % ' '.join(args))
5556
5557 issue_url = Changelist().GetIssueURL()
5558 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005559 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005560 return 1
5561
5562 webbrowser.open(issue_url)
5563 return 0
5564
5565
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005566@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005567def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005568 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005569 parser.add_option('-d', '--dry-run', action='store_true',
5570 help='trigger in dry run mode')
5571 parser.add_option('-c', '--clear', action='store_true',
5572 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005573 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005574 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005575 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005576 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005577 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005578 if args:
5579 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005580 if options.dry_run and options.clear:
5581 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5582
iannuccie53c9352016-08-17 14:40:40 -07005583 cl = Changelist(auth_config=auth_config, issue=options.issue,
5584 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005585 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005586 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005587 elif options.dry_run:
5588 state = _CQState.DRY_RUN
5589 else:
5590 state = _CQState.COMMIT
5591 if not cl.GetIssue():
5592 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005593 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005594 return 0
5595
5596
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005597@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005598def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005599 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005600 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005601 auth.add_auth_options(parser)
5602 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005603 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005604 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005605 if args:
5606 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005607 cl = Changelist(auth_config=auth_config, issue=options.issue,
5608 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005609 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005610 if not cl.GetIssue():
5611 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005612 cl.CloseIssue()
5613 return 0
5614
5615
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005616@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005617def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005618 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005619 parser.add_option(
5620 '--stat',
5621 action='store_true',
5622 dest='stat',
5623 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005624 auth.add_auth_options(parser)
5625 options, args = parser.parse_args(args)
5626 auth_config = auth.extract_auth_config_from_options(options)
5627 if args:
5628 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005629
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005630 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005631 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005632 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005633 if not issue:
5634 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005635
Aaron Gablea718c3e2017-08-28 17:47:28 -07005636 base = cl._GitGetBranchConfigValue('last-upload-hash')
5637 if not base:
5638 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5639 if not base:
5640 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5641 revision_info = detail['revisions'][detail['current_revision']]
5642 fetch_info = revision_info['fetch']['http']
5643 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5644 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005645
Aaron Gablea718c3e2017-08-28 17:47:28 -07005646 cmd = ['git', 'diff']
5647 if options.stat:
5648 cmd.append('--stat')
5649 cmd.append(base)
5650 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005651
5652 return 0
5653
5654
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005655@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005656def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005657 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005658 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005659 '--ignore-current',
5660 action='store_true',
5661 help='Ignore the CL\'s current reviewers and start from scratch.')
5662 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005663 '--no-color',
5664 action='store_true',
5665 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005666 parser.add_option(
5667 '--batch',
5668 action='store_true',
5669 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005670 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005671 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005672 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005673
5674 author = RunGit(['config', 'user.email']).strip() or None
5675
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005676 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005677
5678 if args:
5679 if len(args) > 1:
5680 parser.error('Unknown args')
5681 base_branch = args[0]
5682 else:
5683 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005684 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005685
5686 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005687 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5688
5689 if options.batch:
5690 db = owners.Database(change.RepositoryRoot(), file, os.path)
5691 print('\n'.join(db.reviewers_for(affected_files, author)))
5692 return 0
5693
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005694 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005695 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005696 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005697 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005698 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005699 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005700 disable_color=options.no_color,
5701 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005702
5703
Aiden Bennerc08566e2018-10-03 17:52:42 +00005704def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005705 """Generates a diff command."""
5706 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005707 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5708
5709 if not allow_prefix:
5710 diff_cmd += ['--no-prefix']
5711
5712 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005713
5714 if args:
5715 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005716 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005717 diff_cmd.append(arg)
5718 else:
5719 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005720
5721 return diff_cmd
5722
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005723
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005724def MatchingFileType(file_name, extensions):
5725 """Returns true if the file name ends with one of the given extensions."""
5726 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005727
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005728
enne@chromium.org555cfe42014-01-29 18:21:39 +00005729@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005730@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005731def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005732 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005733 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005734 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005735 parser.add_option('--full', action='store_true',
5736 help='Reformat the full content of all touched files')
5737 parser.add_option('--dry-run', action='store_true',
5738 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005739 parser.add_option('--python', action='store_true',
5740 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005741 parser.add_option('--js', action='store_true',
5742 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005743 parser.add_option('--diff', action='store_true',
5744 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005745 parser.add_option('--presubmit', action='store_true',
5746 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005747 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005748
Daniel Chengc55eecf2016-12-30 03:11:02 -08005749 # Normalize any remaining args against the current path, so paths relative to
5750 # the current directory are still resolved as expected.
5751 args = [os.path.join(os.getcwd(), arg) for arg in args]
5752
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005753 # git diff generates paths against the root of the repository. Change
5754 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005755 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005756 if rel_base_path:
5757 os.chdir(rel_base_path)
5758
digit@chromium.org29e47272013-05-17 17:01:46 +00005759 # Grab the merge-base commit, i.e. the upstream commit of the current
5760 # branch when it was created or the last time it was rebased. This is
5761 # to cover the case where the user may have called "git fetch origin",
5762 # moving the origin branch to a newer commit, but hasn't rebased yet.
5763 upstream_commit = None
5764 cl = Changelist()
5765 upstream_branch = cl.GetUpstreamBranch()
5766 if upstream_branch:
5767 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5768 upstream_commit = upstream_commit.strip()
5769
5770 if not upstream_commit:
5771 DieWithError('Could not find base commit for this branch. '
5772 'Are you in detached state?')
5773
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005774 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5775 diff_output = RunGit(changed_files_cmd)
5776 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005777 # Filter out files deleted by this CL
5778 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005779
Christopher Lamc5ba6922017-01-24 11:19:14 +11005780 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005781 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005782
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005783 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5784 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5785 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005786 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005787
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005788 top_dir = os.path.normpath(
5789 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5790
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005791 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5792 # formatted. This is used to block during the presubmit.
5793 return_value = 0
5794
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005795 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005796 # Locate the clang-format binary in the checkout
5797 try:
5798 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005799 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005800 DieWithError(e)
5801
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005802 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005803 cmd = [clang_format_tool]
5804 if not opts.dry_run and not opts.diff:
5805 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005806 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005807 if opts.diff:
5808 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005809 else:
5810 env = os.environ.copy()
5811 env['PATH'] = str(os.path.dirname(clang_format_tool))
5812 try:
5813 script = clang_format.FindClangFormatScriptInChromiumTree(
5814 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005815 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005816 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005817
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005818 cmd = [sys.executable, script, '-p0']
5819 if not opts.dry_run and not opts.diff:
5820 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005821
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005822 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5823 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005824
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005825 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5826 if opts.diff:
5827 sys.stdout.write(stdout)
5828 if opts.dry_run and len(stdout) > 0:
5829 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005830
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005831 # Similar code to above, but using yapf on .py files rather than clang-format
5832 # on C/C++ files
Aiden Bennerc08566e2018-10-03 17:52:42 +00005833 if opts.python and python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005834 yapf_tool = gclient_utils.FindExecutable('yapf')
5835 if yapf_tool is None:
5836 DieWithError('yapf not found in PATH')
5837
Aiden Bennerc08566e2018-10-03 17:52:42 +00005838 # If we couldn't find a yapf file we'll default to the chromium style
5839 # specified in depot_tools.
5840 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5841 chromium_default_yapf_style = os.path.join(depot_tools_path,
5842 YAPF_CONFIG_FILENAME)
5843
5844 # Note: yapf still seems to fix indentation of the entire file
5845 # even if line ranges are specified.
5846 # See https://github.com/google/yapf/issues/499
5847 if not opts.full:
5848 py_line_diffs = _ComputeDiffLineRanges(python_diff_files, upstream_commit)
5849
5850 # Used for caching.
5851 yapf_configs = {}
5852 for f in python_diff_files:
5853 # Find the yapf style config for the current file, defaults to depot
5854 # tools default.
5855 yapf_config = _FindYapfConfigFile(
5856 os.path.abspath(f), yapf_configs, top_dir,
5857 chromium_default_yapf_style)
5858
5859 cmd = [yapf_tool, '--style', yapf_config, f]
5860
5861 has_formattable_lines = False
5862 if not opts.full:
5863 # Only run yapf over changed line ranges.
5864 for diff_start, diff_len in py_line_diffs[f]:
5865 diff_end = diff_start + diff_len - 1
5866 # Yapf errors out if diff_end < diff_start but this
5867 # is a valid line range diff for a removal.
5868 if diff_end >= diff_start:
5869 has_formattable_lines = True
5870 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5871 # If all line diffs were removals we have nothing to format.
5872 if not has_formattable_lines:
5873 continue
5874
5875 if opts.diff or opts.dry_run:
5876 cmd += ['--diff']
5877 # Will return non-zero exit code if non-empty diff.
5878 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5879 if opts.diff:
5880 sys.stdout.write(stdout)
5881 elif len(stdout) > 0:
5882 return_value = 2
5883 else:
5884 cmd += ['-i']
5885 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005886
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005887 # Dart's formatter does not have the nice property of only operating on
5888 # modified chunks, so hard code full.
5889 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005890 try:
5891 command = [dart_format.FindDartFmtToolInChromiumTree()]
5892 if not opts.dry_run and not opts.diff:
5893 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005894 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005895
ppi@chromium.org6593d932016-03-03 15:41:15 +00005896 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005897 if opts.dry_run and stdout:
5898 return_value = 2
5899 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005900 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5901 'found in this checkout. Files in other languages are still '
5902 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005903
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005904 # Format GN build files. Always run on full build files for canonical form.
5905 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005906 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005907 if opts.dry_run or opts.diff:
5908 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005909 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005910 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5911 shell=sys.platform == 'win32',
5912 cwd=top_dir)
5913 if opts.dry_run and gn_ret == 2:
5914 return_value = 2 # Not formatted.
5915 elif opts.diff and gn_ret == 2:
5916 # TODO this should compute and print the actual diff.
5917 print("This change has GN build file diff for " + gn_diff_file)
5918 elif gn_ret != 0:
5919 # For non-dry run cases (and non-2 return values for dry-run), a
5920 # nonzero error code indicates a failure, probably because the file
5921 # doesn't parse.
5922 DieWithError("gn format failed on " + gn_diff_file +
5923 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005924
Ilya Shermane081cbe2017-08-15 17:51:04 -07005925 # Skip the metrics formatting from the global presubmit hook. These files have
5926 # a separate presubmit hook that issues an error if the files need formatting,
5927 # whereas the top-level presubmit script merely issues a warning. Formatting
5928 # these files is somewhat slow, so it's important not to duplicate the work.
5929 if not opts.presubmit:
5930 for xml_dir in GetDirtyMetricsDirs(diff_files):
5931 tool_dir = os.path.join(top_dir, xml_dir)
5932 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5933 if opts.dry_run or opts.diff:
5934 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005935 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005936 if opts.diff:
5937 sys.stdout.write(stdout)
5938 if opts.dry_run and stdout:
5939 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005940
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005941 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005942
Steven Holte2e664bf2017-04-21 13:10:47 -07005943def GetDirtyMetricsDirs(diff_files):
5944 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5945 metrics_xml_dirs = [
5946 os.path.join('tools', 'metrics', 'actions'),
5947 os.path.join('tools', 'metrics', 'histograms'),
5948 os.path.join('tools', 'metrics', 'rappor'),
5949 os.path.join('tools', 'metrics', 'ukm')]
5950 for xml_dir in metrics_xml_dirs:
5951 if any(file.startswith(xml_dir) for file in xml_diff_files):
5952 yield xml_dir
5953
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005954
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005955@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005956@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005957def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005958 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005959 _, args = parser.parse_args(args)
5960
5961 if len(args) != 1:
5962 parser.print_help()
5963 return 1
5964
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005965 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005966 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005967 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005968
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005969 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005970
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005971 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005972 output = RunGit(['config', '--local', '--get-regexp',
5973 r'branch\..*\.%s' % issueprefix],
5974 error_ok=True)
5975 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005976 if issue == target_issue:
5977 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005978
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005979 branches = []
5980 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005981 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005982 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005983 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005984 return 1
5985 if len(branches) == 1:
5986 RunGit(['checkout', branches[0]])
5987 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005988 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005989 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005990 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005991 which = raw_input('Choose by index: ')
5992 try:
5993 RunGit(['checkout', branches[int(which)]])
5994 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005995 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005996 return 1
5997
5998 return 0
5999
6000
maruel@chromium.org29404b52014-09-08 22:58:00 +00006001def CMDlol(parser, args):
6002 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006003 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006004 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6005 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6006 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006007 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006008 return 0
6009
6010
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006011class OptionParser(optparse.OptionParser):
6012 """Creates the option parse and add --verbose support."""
6013 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006014 optparse.OptionParser.__init__(
6015 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006016 self.add_option(
6017 '-v', '--verbose', action='count', default=0,
6018 help='Use 2 times for more debugging info')
6019
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006020 def parse_args(self, args=None, _values=None):
6021 # Create an optparse.Values object that will store only the actual passed
6022 # options, without the defaults.
6023 actual_options = optparse.Values()
6024 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6025 # Create an optparse.Values object with the default options.
6026 options = optparse.Values(self.get_default_values().__dict__)
6027 # Update it with the options passed by the user.
6028 options._update_careful(actual_options.__dict__)
6029 # Store the options passed by the user in an _actual_options attribute.
6030 # We store only the keys, and not the values, since the values can contain
6031 # arbitrary information, which might be PII.
6032 metrics.collector.add('arguments', actual_options.__dict__.keys())
6033
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006034 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006035 logging.basicConfig(
6036 level=levels[min(options.verbose, len(levels) - 1)],
6037 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6038 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00006039
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006040 return options, args
6041
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006042
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006043def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006044 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006045 print('\nYour python version %s is unsupported, please upgrade.\n' %
6046 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006047 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006048
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006049 # Reload settings.
6050 global settings
6051 settings = Settings()
6052
Edward Lemurad463c92018-07-25 21:31:23 +00006053 if not metrics.DISABLE_METRICS_COLLECTION:
6054 metrics.collector.add('project_urls', [settings.GetViewVCUrl().strip('/+')])
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006055 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006056 dispatcher = subcommand.CommandDispatcher(__name__)
6057 try:
6058 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006059 except auth.AuthenticationError as e:
6060 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006061 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006062 if e.code != 500:
6063 raise
6064 DieWithError(
6065 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6066 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006067 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006068
6069
6070if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006071 # These affect sys.stdout so do it outside of main() to simplify mocks in
6072 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006073 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006074 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00006075 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00006076 sys.exit(main(sys.argv[1:]))