blob: 584e7cac71dd22795a2c7451ee37bc20806691bf [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000034import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000035import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000036import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000037import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038
39try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080040 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041except ImportError:
42 pass
43
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000044from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000045from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000058import metrics
piman@chromium.org336f9122014-09-04 02:16:55 +000059import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000060import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000062import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040064import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067import watchlists
68
tandrii7400cf02016-06-21 08:48:07 -070069__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000070
tandrii9d2c7a32016-06-22 03:42:45 -070071COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070072DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080073POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000075REFS_THAT_ALIAS_TO_OTHER_REFS = {
76 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
77 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
78}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079
thestig@chromium.org44202a22014-03-11 19:22:18 +000080# Valid extensions for files we want to lint.
81DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
82DEFAULT_LINT_IGNORE_REGEX = r"$^"
83
Aiden Bennerc08566e2018-10-03 17:52:42 +000084# File name for yapf style config files.
85YAPF_CONFIG_FILENAME = '.style.yapf'
86
borenet6c0efe62016-10-19 08:13:29 -070087# Buildbucket master name prefix.
88MASTER_PREFIX = 'master.'
89
Edward Lemur83bd7f42018-10-10 00:14:21 +000090# TODO(crbug.com/881860): Remove
91# Log gerrit failures to a gerrit_util.GERRIT_ERR_LOG_FILE.
92GERRIT_ERR_LOGGER = logging.getLogger('GerritErrorLogs')
93
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000094# Shortcut since it quickly becomes redundant.
95Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000096
maruel@chromium.orgddd59412011-11-30 14:20:38 +000097# Initialized in main()
98settings = None
99
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100100# Used by tests/git_cl_test.py to add extra logging.
101# Inside the weirdly failing test, add this:
102# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700103# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100104_IS_BEING_TESTED = False
105
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000106
Christopher Lamf732cd52017-01-24 12:40:11 +1100107def DieWithError(message, change_desc=None):
108 if change_desc:
109 SaveDescriptionBackup(change_desc)
110
vapiera7fbd5a2016-06-16 09:17:49 -0700111 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112 sys.exit(1)
113
114
Christopher Lamf732cd52017-01-24 12:40:11 +1100115def SaveDescriptionBackup(change_desc):
116 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000117 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100118 backup_file = open(backup_path, 'w')
119 backup_file.write(change_desc.description)
120 backup_file.close()
121
122
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123def GetNoGitPagerEnv():
124 env = os.environ.copy()
125 # 'cat' is a magical git string that disables pagers on all platforms.
126 env['GIT_PAGER'] = 'cat'
127 return env
128
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000129
bsep@chromium.org627d9002016-04-29 00:00:52 +0000130def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000132 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000133 except subprocess2.CalledProcessError as e:
134 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000135 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000136 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000137 'Command "%s" failed.\n%s' % (
138 ' '.join(args), error_message or e.stdout or ''))
139 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000140
141
142def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000143 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000144 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000145
146
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000147def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000148 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700149 if suppress_stderr:
150 stderr = subprocess2.VOID
151 else:
152 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000153 try:
tandrii5d48c322016-08-18 16:19:37 -0700154 (out, _), code = subprocess2.communicate(['git'] + args,
155 env=GetNoGitPagerEnv(),
156 stdout=subprocess2.PIPE,
157 stderr=stderr)
158 return code, out
159 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900160 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700161 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000162
163
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000164def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000165 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000166 return RunGitWithCode(args, suppress_stderr=True)[1]
167
168
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000169def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000170 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000171 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000172 return (version.startswith(prefix) and
173 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000174
175
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000176def BranchExists(branch):
177 """Return True if specified branch exists."""
178 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
179 suppress_stderr=True)
180 return not code
181
182
tandrii2a16b952016-10-19 07:09:44 -0700183def time_sleep(seconds):
184 # Use this so that it can be mocked in tests without interfering with python
185 # system machinery.
186 import time # Local import to discourage others from importing time globally.
187 return time.sleep(seconds)
188
189
maruel@chromium.org90541732011-04-01 17:54:18 +0000190def ask_for_data(prompt):
191 try:
192 return raw_input(prompt)
193 except KeyboardInterrupt:
194 # Hide the exception.
195 sys.exit(1)
196
197
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100198def confirm_or_exit(prefix='', action='confirm'):
199 """Asks user to press enter to continue or press Ctrl+C to abort."""
200 if not prefix or prefix.endswith('\n'):
201 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100202 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100203 mid = ' Press'
204 elif prefix.endswith(' '):
205 mid = 'press'
206 else:
207 mid = ' press'
208 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
209
210
211def ask_for_explicit_yes(prompt):
212 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
213 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
214 while True:
215 if 'yes'.startswith(result):
216 return True
217 if 'no'.startswith(result):
218 return False
219 result = ask_for_data('Please, type yes or no: ').lower()
220
221
tandrii5d48c322016-08-18 16:19:37 -0700222def _git_branch_config_key(branch, key):
223 """Helper method to return Git config key for a branch."""
224 assert branch, 'branch name is required to set git config for it'
225 return 'branch.%s.%s' % (branch, key)
226
227
228def _git_get_branch_config_value(key, default=None, value_type=str,
229 branch=False):
230 """Returns git config value of given or current branch if any.
231
232 Returns default in all other cases.
233 """
234 assert value_type in (int, str, bool)
235 if branch is False: # Distinguishing default arg value from None.
236 branch = GetCurrentBranch()
237
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000238 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700239 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000240
tandrii5d48c322016-08-18 16:19:37 -0700241 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700242 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700243 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700244 # git config also has --int, but apparently git config suffers from integer
245 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700246 args.append(_git_branch_config_key(branch, key))
247 code, out = RunGitWithCode(args)
248 if code == 0:
249 value = out.strip()
250 if value_type == int:
251 return int(value)
252 if value_type == bool:
253 return bool(value.lower() == 'true')
254 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255 return default
256
257
tandrii5d48c322016-08-18 16:19:37 -0700258def _git_set_branch_config_value(key, value, branch=None, **kwargs):
259 """Sets the value or unsets if it's None of a git branch config.
260
261 Valid, though not necessarily existing, branch must be provided,
262 otherwise currently checked out branch is used.
263 """
264 if not branch:
265 branch = GetCurrentBranch()
266 assert branch, 'a branch name OR currently checked out branch is required'
267 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700268 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700269 if value is None:
270 args.append('--unset')
271 elif isinstance(value, bool):
272 args.append('--bool')
273 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700274 else:
tandrii33a46ff2016-08-23 05:53:40 -0700275 # git config also has --int, but apparently git config suffers from integer
276 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700277 value = str(value)
278 args.append(_git_branch_config_key(branch, key))
279 if value is not None:
280 args.append(value)
281 RunGit(args, **kwargs)
282
283
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100284def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700285 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100286
287 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
288 """
289 # Git also stores timezone offset, but it only affects visual display,
290 # actual point in time is defined by this timestamp only.
291 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
292
293
294def _git_amend_head(message, committer_timestamp):
295 """Amends commit with new message and desired committer_timestamp.
296
297 Sets committer timezone to UTC.
298 """
299 env = os.environ.copy()
300 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
301 return RunGit(['commit', '--amend', '-m', message], env=env)
302
303
machenbach@chromium.org45453142015-09-15 08:45:22 +0000304def _get_properties_from_options(options):
305 properties = dict(x.split('=', 1) for x in options.properties)
306 for key, val in properties.iteritems():
307 try:
308 properties[key] = json.loads(val)
309 except ValueError:
310 pass # If a value couldn't be evaluated, treat it as a string.
311 return properties
312
313
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000314def _prefix_master(master):
315 """Convert user-specified master name to full master name.
316
317 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
318 name, while the developers always use shortened master name
319 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
320 function does the conversion for buildbucket migration.
321 """
borenet6c0efe62016-10-19 08:13:29 -0700322 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000323 return master
borenet6c0efe62016-10-19 08:13:29 -0700324 return '%s%s' % (MASTER_PREFIX, master)
325
326
327def _unprefix_master(bucket):
328 """Convert bucket name to shortened master name.
329
330 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
331 name, while the developers always use shortened master name
332 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
333 function does the conversion for buildbucket migration.
334 """
335 if bucket.startswith(MASTER_PREFIX):
336 return bucket[len(MASTER_PREFIX):]
337 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338
339
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000340def _buildbucket_retry(operation_name, http, *args, **kwargs):
341 """Retries requests to buildbucket service and returns parsed json content."""
342 try_count = 0
343 while True:
344 response, content = http.request(*args, **kwargs)
345 try:
346 content_json = json.loads(content)
347 except ValueError:
348 content_json = None
349
350 # Buildbucket could return an error even if status==200.
351 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000352 error = content_json.get('error')
353 if error.get('code') == 403:
354 raise BuildbucketResponseException(
355 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000356 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000357 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 raise BuildbucketResponseException(msg)
359
360 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700361 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 raise BuildbucketResponseException(
363 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700364 'Please file bugs at http://crbug.com, '
365 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366 content)
367 return content_json
368 if response.status < 500 or try_count >= 2:
369 raise httplib2.HttpLib2Error(content)
370
371 # status >= 500 means transient failures.
372 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700373 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374 try_count += 1
375 assert False, 'unreachable'
376
377
qyearsley1fdfcb62016-10-24 13:22:03 -0700378def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700379 """Returns a dict mapping bucket names to builders and tests,
380 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700381 """
qyearsleydd49f942016-10-28 11:57:22 -0700382 # If no bots are listed, we try to get a set of builders and tests based
383 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700384 if not options.bot:
385 change = changelist.GetChange(
386 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700387 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700388 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700389 change=change,
390 changed_files=change.LocalPaths(),
391 repository_root=settings.GetRoot(),
392 default_presubmit=None,
393 project=None,
394 verbose=options.verbose,
395 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700396 if masters is None:
397 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100398 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700399
qyearsley1fdfcb62016-10-24 13:22:03 -0700400 if options.bucket:
401 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700402 if options.master:
403 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700404
qyearsleydd49f942016-10-28 11:57:22 -0700405 # If bots are listed but no master or bucket, then we need to find out
406 # the corresponding master for each bot.
407 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
408 if error_message:
409 option_parser.error(
410 'Tryserver master cannot be found because: %s\n'
411 'Please manually specify the tryserver master, e.g. '
412 '"-m tryserver.chromium.linux".' % error_message)
413 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700414
415
qyearsley123a4682016-10-26 09:12:17 -0700416def _get_bucket_map_for_builders(builders):
417 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700418 map_url = 'https://builders-map.appspot.com/'
419 try:
qyearsley123a4682016-10-26 09:12:17 -0700420 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700421 except urllib2.URLError as e:
422 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
423 (map_url, e))
424 except ValueError as e:
425 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700426 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700427 return None, 'Failed to build master map.'
428
qyearsley123a4682016-10-26 09:12:17 -0700429 bucket_map = {}
430 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800431 bucket = builders_map.get(builder, {}).get('bucket')
432 if bucket:
433 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700434 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700435
436
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800437def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700438 """Sends a request to Buildbucket to trigger try jobs for a changelist.
439
440 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700441 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700442 changelist: Changelist that the try jobs are associated with.
443 buckets: A nested dict mapping bucket names to builders to tests.
444 options: Command-line options.
445 """
tandriide281ae2016-10-12 06:02:30 -0700446 assert changelist.GetIssue(), 'CL must be uploaded first'
447 codereview_url = changelist.GetCodereviewServer()
448 assert codereview_url, 'CL must be uploaded first'
449 patchset = patchset or changelist.GetMostRecentPatchset()
450 assert patchset, 'CL must be uploaded first'
451
452 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700453 # Cache the buildbucket credentials under the codereview host key, so that
454 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700455 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000456 http = authenticator.authorize(httplib2.Http())
457 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700458
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459 buildbucket_put_url = (
460 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000461 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000462 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700463 hostname=codereview_host,
464 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000465 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700466
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700467 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800468 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700469 if options.clobber:
470 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700471 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700472 if extra_properties:
473 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000474
475 batch_req_body = {'builds': []}
476 print_text = []
477 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700478 for bucket, builders_and_tests in sorted(buckets.iteritems()):
479 print_text.append('Bucket: %s' % bucket)
480 master = None
481 if bucket.startswith(MASTER_PREFIX):
482 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000483 for builder, tests in sorted(builders_and_tests.iteritems()):
484 print_text.append(' %s: %s' % (builder, tests))
485 parameters = {
486 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000487 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100488 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000489 'revision': options.revision,
490 }],
tandrii8c5a3532016-11-04 07:52:02 -0700491 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000492 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000493 if 'presubmit' in builder.lower():
494 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000495 if tests:
496 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700497
498 tags = [
499 'builder:%s' % builder,
500 'buildset:%s' % buildset,
501 'user_agent:git_cl_try',
502 ]
503 if master:
504 parameters['properties']['master'] = master
505 tags.append('master:%s' % master)
506
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000507 batch_req_body['builds'].append(
508 {
509 'bucket': bucket,
510 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700512 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000513 }
514 )
515
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000516 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700517 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000518 http,
519 buildbucket_put_url,
520 'PUT',
521 body=json.dumps(batch_req_body),
522 headers={'Content-Type': 'application/json'}
523 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000524 print_text.append('To see results here, run: git cl try-results')
525 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700526 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000527
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000528
tandrii221ab252016-10-06 08:12:04 -0700529def fetch_try_jobs(auth_config, changelist, buildbucket_host,
530 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700531 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000532
qyearsley53f48a12016-09-01 10:45:13 -0700533 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534 """
tandrii221ab252016-10-06 08:12:04 -0700535 assert buildbucket_host
536 assert changelist.GetIssue(), 'CL must be uploaded first'
537 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
538 patchset = patchset or changelist.GetMostRecentPatchset()
539 assert patchset, 'CL must be uploaded first'
540
541 codereview_url = changelist.GetCodereviewServer()
542 codereview_host = urlparse.urlparse(codereview_url).hostname
543 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 if authenticator.has_cached_credentials():
545 http = authenticator.authorize(httplib2.Http())
546 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700547 print('Warning: Some results might be missing because %s' %
548 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700549 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 http = httplib2.Http()
551
552 http.force_exception_to_status_code = True
553
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000554 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700555 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700557 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 params = {'tag': 'buildset:%s' % buildset}
559
560 builds = {}
561 while True:
562 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700563 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700565 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566 for build in content.get('builds', []):
567 builds[build['id']] = build
568 if 'next_cursor' in content:
569 params['start_cursor'] = content['next_cursor']
570 else:
571 break
572 return builds
573
574
qyearsleyeab3c042016-08-24 09:18:28 -0700575def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576 """Prints nicely result of fetch_try_jobs."""
577 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700578 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 return
580
581 # Make a copy, because we'll be modifying builds dictionary.
582 builds = builds.copy()
583 builder_names_cache = {}
584
585 def get_builder(b):
586 try:
587 return builder_names_cache[b['id']]
588 except KeyError:
589 try:
590 parameters = json.loads(b['parameters_json'])
591 name = parameters['builder_name']
592 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700593 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700594 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000595 name = None
596 builder_names_cache[b['id']] = name
597 return name
598
599 def get_bucket(b):
600 bucket = b['bucket']
601 if bucket.startswith('master.'):
602 return bucket[len('master.'):]
603 return bucket
604
605 if options.print_master:
606 name_fmt = '%%-%ds %%-%ds' % (
607 max(len(str(get_bucket(b))) for b in builds.itervalues()),
608 max(len(str(get_builder(b))) for b in builds.itervalues()))
609 def get_name(b):
610 return name_fmt % (get_bucket(b), get_builder(b))
611 else:
612 name_fmt = '%%-%ds' % (
613 max(len(str(get_builder(b))) for b in builds.itervalues()))
614 def get_name(b):
615 return name_fmt % get_builder(b)
616
617 def sort_key(b):
618 return b['status'], b.get('result'), get_name(b), b.get('url')
619
620 def pop(title, f, color=None, **kwargs):
621 """Pop matching builds from `builds` dict and print them."""
622
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000623 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000624 colorize = str
625 else:
626 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
627
628 result = []
629 for b in builds.values():
630 if all(b.get(k) == v for k, v in kwargs.iteritems()):
631 builds.pop(b['id'])
632 result.append(b)
633 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700634 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000635 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700636 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000637
638 total = len(builds)
639 pop(status='COMPLETED', result='SUCCESS',
640 title='Successes:', color=Fore.GREEN,
641 f=lambda b: (get_name(b), b.get('url')))
642 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
643 title='Infra Failures:', color=Fore.MAGENTA,
644 f=lambda b: (get_name(b), b.get('url')))
645 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
646 title='Failures:', color=Fore.RED,
647 f=lambda b: (get_name(b), b.get('url')))
648 pop(status='COMPLETED', result='CANCELED',
649 title='Canceled:', color=Fore.MAGENTA,
650 f=lambda b: (get_name(b),))
651 pop(status='COMPLETED', result='FAILURE',
652 failure_reason='INVALID_BUILD_DEFINITION',
653 title='Wrong master/builder name:', color=Fore.MAGENTA,
654 f=lambda b: (get_name(b),))
655 pop(status='COMPLETED', result='FAILURE',
656 title='Other failures:',
657 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
658 pop(status='COMPLETED',
659 title='Other finished:',
660 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
661 pop(status='STARTED',
662 title='Started:', color=Fore.YELLOW,
663 f=lambda b: (get_name(b), b.get('url')))
664 pop(status='SCHEDULED',
665 title='Scheduled:',
666 f=lambda b: (get_name(b), 'id=%s' % b['id']))
667 # The last section is just in case buildbucket API changes OR there is a bug.
668 pop(title='Other:',
669 f=lambda b: (get_name(b), 'id=%s' % b['id']))
670 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700671 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000672
673
Aiden Bennerc08566e2018-10-03 17:52:42 +0000674def _ComputeDiffLineRanges(files, upstream_commit):
675 """Gets the changed line ranges for each file since upstream_commit.
676
677 Parses a git diff on provided files and returns a dict that maps a file name
678 to an ordered list of range tuples in the form (start_line, count).
679 Ranges are in the same format as a git diff.
680 """
681 # If files is empty then diff_output will be a full diff.
682 if len(files) == 0:
683 return {}
684
685 # Take diff and find the line ranges where there are changes.
686 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
687 diff_output = RunGit(diff_cmd)
688
689 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
690 # 2 capture groups
691 # 0 == fname of diff file
692 # 1 == 'diff_start,diff_count' or 'diff_start'
693 # will match each of
694 # diff --git a/foo.foo b/foo.py
695 # @@ -12,2 +14,3 @@
696 # @@ -12,2 +17 @@
697 # running re.findall on the above string with pattern will give
698 # [('foo.py', ''), ('', '14,3'), ('', '17')]
699
700 curr_file = None
701 line_diffs = {}
702 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
703 if match[0] != '':
704 # Will match the second filename in diff --git a/a.py b/b.py.
705 curr_file = match[0]
706 line_diffs[curr_file] = []
707 else:
708 # Matches +14,3
709 if ',' in match[1]:
710 diff_start, diff_count = match[1].split(',')
711 else:
712 # Single line changes are of the form +12 instead of +12,1.
713 diff_start = match[1]
714 diff_count = 1
715
716 diff_start = int(diff_start)
717 diff_count = int(diff_count)
718
719 # If diff_count == 0 this is a removal we can ignore.
720 line_diffs[curr_file].append((diff_start, diff_count))
721
722 return line_diffs
723
724
725def _FindYapfConfigFile(fpath,
726 yapf_config_cache,
727 top_dir=None,
728 default_style=None):
729 """Checks if a yapf file is in any parent directory of fpath until top_dir.
730
731 Recursively checks parent directories to find yapf file
732 and if no yapf file is found returns default_style.
733 Uses yapf_config_cache as a cache for previously found files.
734 """
735 # Return result if we've already computed it.
736 if fpath in yapf_config_cache:
737 return yapf_config_cache[fpath]
738
739 # Check if there is a style file in the current directory.
740 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
741 dirname = os.path.dirname(fpath)
742 if os.path.isfile(yapf_file):
743 ret = yapf_file
744 elif fpath == top_dir or dirname == fpath:
745 # If we're at the top level directory, or if we're at root
746 # use the chromium default yapf style.
747 ret = default_style
748 else:
749 # Otherwise recurse on the current directory.
750 ret = _FindYapfConfigFile(dirname, yapf_config_cache, top_dir,
751 default_style)
752 yapf_config_cache[fpath] = ret
753 return ret
754
755
qyearsley53f48a12016-09-01 10:45:13 -0700756def write_try_results_json(output_file, builds):
757 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
758
759 The input |builds| dict is assumed to be generated by Buildbucket.
760 Buildbucket documentation: http://goo.gl/G0s101
761 """
762
763 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800764 """Extracts some of the information from one build dict."""
765 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700766 return {
767 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700768 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800769 'builder_name': parameters.get('builder_name'),
770 'created_ts': build.get('created_ts'),
771 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700772 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800773 'result': build.get('result'),
774 'status': build.get('status'),
775 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700776 'url': build.get('url'),
777 }
778
779 converted = []
780 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000781 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700782 write_json(output_file, converted)
783
784
Aaron Gable13101a62018-02-09 13:20:41 -0800785def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000786 """Prints statistics about the change to the user."""
787 # --no-ext-diff is broken in some versions of Git, so try to work around
788 # this by overriding the environment (but there is still a problem if the
789 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000790 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000791 if 'GIT_EXTERNAL_DIFF' in env:
792 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000793
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000794 try:
795 stdout = sys.stdout.fileno()
796 except AttributeError:
797 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000798 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800799 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000800 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000801
802
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000803class BuildbucketResponseException(Exception):
804 pass
805
806
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807class Settings(object):
808 def __init__(self):
809 self.default_server = None
810 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000811 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 self.tree_status_url = None
813 self.viewvc_url = None
814 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000815 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000816 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000817 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000818 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000819 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000820 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821
822 def LazyUpdateIfNeeded(self):
823 """Updates the settings from a codereview.settings file, if available."""
824 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000825 # The only value that actually changes the behavior is
826 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000827 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000828 error_ok=True
829 ).strip().lower()
830
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000831 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000832 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000833 LoadCodereviewSettingsFromFile(cr_settings_file)
834 self.updated = True
835
836 def GetDefaultServerUrl(self, error_ok=False):
837 if not self.default_server:
838 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000839 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000840 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 if error_ok:
842 return self.default_server
843 if not self.default_server:
844 error_message = ('Could not find settings file. You must configure '
845 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000846 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000847 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848 return self.default_server
849
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000850 @staticmethod
851 def GetRelativeRoot():
852 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000853
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000855 if self.root is None:
856 self.root = os.path.abspath(self.GetRelativeRoot())
857 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000859 def GetGitMirror(self, remote='origin'):
860 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000861 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000862 if not os.path.isdir(local_url):
863 return None
864 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
865 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100866 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100867 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000868 if mirror.exists():
869 return mirror
870 return None
871
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 def GetTreeStatusUrl(self, error_ok=False):
873 if not self.tree_status_url:
874 error_message = ('You must configure your tree status URL by running '
875 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000876 self.tree_status_url = self._GetRietveldConfig(
877 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878 return self.tree_status_url
879
880 def GetViewVCUrl(self):
881 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000882 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883 return self.viewvc_url
884
rmistry@google.com90752582014-01-14 21:04:50 +0000885 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000886 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000887
rmistry@google.com78948ed2015-07-08 23:09:57 +0000888 def GetIsSkipDependencyUpload(self, branch_name):
889 """Returns true if specified branch should skip dep uploads."""
890 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
891 error_ok=True)
892
rmistry@google.com5626a922015-02-26 14:03:30 +0000893 def GetRunPostUploadHook(self):
894 run_post_upload_hook = self._GetRietveldConfig(
895 'run-post-upload-hook', error_ok=True)
896 return run_post_upload_hook == "True"
897
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000898 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000899 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000900
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000901 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000902 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000903
ukai@chromium.orge8077812012-02-03 03:41:46 +0000904 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700905 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000906 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700907 self.is_gerrit = (
908 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000909 return self.is_gerrit
910
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000911 def GetSquashGerritUploads(self):
912 """Return true if uploads to Gerrit should be squashed by default."""
913 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700914 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
915 if self.squash_gerrit_uploads is None:
916 # Default is squash now (http://crbug.com/611892#c23).
917 self.squash_gerrit_uploads = not (
918 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
919 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000920 return self.squash_gerrit_uploads
921
tandriia60502f2016-06-20 02:01:53 -0700922 def GetSquashGerritUploadsOverride(self):
923 """Return True or False if codereview.settings should be overridden.
924
925 Returns None if no override has been defined.
926 """
927 # See also http://crbug.com/611892#c23
928 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
929 error_ok=True).strip()
930 if result == 'true':
931 return True
932 if result == 'false':
933 return False
934 return None
935
tandrii@chromium.org28253532016-04-14 13:46:56 +0000936 def GetGerritSkipEnsureAuthenticated(self):
937 """Return True if EnsureAuthenticated should not be done for Gerrit
938 uploads."""
939 if self.gerrit_skip_ensure_authenticated is None:
940 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000941 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000942 error_ok=True).strip() == 'true')
943 return self.gerrit_skip_ensure_authenticated
944
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000945 def GetGitEditor(self):
946 """Return the editor specified in the git config, or None if none is."""
947 if self.git_editor is None:
948 self.git_editor = self._GetConfig('core.editor', error_ok=True)
949 return self.git_editor or None
950
thestig@chromium.org44202a22014-03-11 19:22:18 +0000951 def GetLintRegex(self):
952 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
953 DEFAULT_LINT_REGEX)
954
955 def GetLintIgnoreRegex(self):
956 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
957 DEFAULT_LINT_IGNORE_REGEX)
958
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000959 def GetProject(self):
960 if not self.project:
961 self.project = self._GetRietveldConfig('project', error_ok=True)
962 return self.project
963
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000964 def _GetRietveldConfig(self, param, **kwargs):
965 return self._GetConfig('rietveld.' + param, **kwargs)
966
rmistry@google.com78948ed2015-07-08 23:09:57 +0000967 def _GetBranchConfig(self, branch_name, param, **kwargs):
968 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
969
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970 def _GetConfig(self, param, **kwargs):
971 self.LazyUpdateIfNeeded()
972 return RunGit(['config', param], **kwargs).strip()
973
974
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100975@contextlib.contextmanager
976def _get_gerrit_project_config_file(remote_url):
977 """Context manager to fetch and store Gerrit's project.config from
978 refs/meta/config branch and store it in temp file.
979
980 Provides a temporary filename or None if there was error.
981 """
982 error, _ = RunGitWithCode([
983 'fetch', remote_url,
984 '+refs/meta/config:refs/git_cl/meta/config'])
985 if error:
986 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700987 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100988 (remote_url, error))
989 yield None
990 return
991
992 error, project_config_data = RunGitWithCode(
993 ['show', 'refs/git_cl/meta/config:project.config'])
994 if error:
995 print('WARNING: project.config file not found')
996 yield None
997 return
998
999 with gclient_utils.temporary_directory() as tempdir:
1000 project_config_file = os.path.join(tempdir, 'project.config')
1001 gclient_utils.FileWrite(project_config_file, project_config_data)
1002 yield project_config_file
1003
1004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005def ShortBranchName(branch):
1006 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001007 return branch.replace('refs/heads/', '', 1)
1008
1009
1010def GetCurrentBranchRef():
1011 """Returns branch ref (e.g., refs/heads/master) or None."""
1012 return RunGit(['symbolic-ref', 'HEAD'],
1013 stderr=subprocess2.VOID, error_ok=True).strip() or None
1014
1015
1016def GetCurrentBranch():
1017 """Returns current branch or None.
1018
1019 For refs/heads/* branches, returns just last part. For others, full ref.
1020 """
1021 branchref = GetCurrentBranchRef()
1022 if branchref:
1023 return ShortBranchName(branchref)
1024 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025
1026
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001027class _CQState(object):
1028 """Enum for states of CL with respect to Commit Queue."""
1029 NONE = 'none'
1030 DRY_RUN = 'dry_run'
1031 COMMIT = 'commit'
1032
1033 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1034
1035
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001036class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001037 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001038 self.issue = issue
1039 self.patchset = patchset
1040 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001041 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001042 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001043
1044 @property
1045 def valid(self):
1046 return self.issue is not None
1047
1048
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001049def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001050 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1051 fail_result = _ParsedIssueNumberArgument()
1052
1053 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001054 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001055 if not arg.startswith('http'):
1056 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001057
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001058 url = gclient_utils.UpgradeToHttps(arg)
1059 try:
1060 parsed_url = urlparse.urlparse(url)
1061 except ValueError:
1062 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001063
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001064 if codereview is not None:
1065 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1066 return parsed or fail_result
1067
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001068 results = {}
1069 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1070 parsed = cls.ParseIssueURL(parsed_url)
1071 if parsed is not None:
1072 results[name] = parsed
1073
1074 if not results:
1075 return fail_result
1076 if len(results) == 1:
1077 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001078
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001079 return results['gerrit']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001080
1081
Aaron Gablea45ee112016-11-22 15:14:38 -08001082class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001083 def __init__(self, issue, url):
1084 self.issue = issue
1085 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001086 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001087
1088 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001089 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001090 self.issue, self.url)
1091
1092
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001093_CommentSummary = collections.namedtuple(
1094 '_CommentSummary', ['date', 'message', 'sender',
1095 # TODO(tandrii): these two aren't known in Gerrit.
1096 'approval', 'disapproval'])
1097
1098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001100 """Changelist works with one changelist in local branch.
1101
1102 Supports two codereview backends: Rietveld or Gerrit, selected at object
1103 creation.
1104
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001105 Notes:
1106 * Not safe for concurrent multi-{thread,process} use.
1107 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001108 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001109 """
1110
1111 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1112 """Create a new ChangeList instance.
1113
1114 If issue is given, the codereview must be given too.
1115
1116 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1117 Otherwise, it's decided based on current configuration of the local branch,
1118 with default being 'rietveld' for backwards compatibility.
1119 See _load_codereview_impl for more details.
1120
1121 **kwargs will be passed directly to codereview implementation.
1122 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001124 global settings
1125 if not settings:
1126 # Happens when git_cl.py is used as a utility library.
1127 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001128
1129 if issue:
1130 assert codereview, 'codereview must be known, if issue is known'
1131
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 self.branchref = branchref
1133 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001134 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 self.branch = ShortBranchName(self.branchref)
1136 else:
1137 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001139 self.lookedup_issue = False
1140 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 self.has_description = False
1142 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001143 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001144 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001145 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001146 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001147 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001148 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001149
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001150 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001151 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001152 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001153 assert self._codereview_impl
1154 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001155
1156 def _load_codereview_impl(self, codereview=None, **kwargs):
1157 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001158 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1159 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1160 self._codereview = codereview
1161 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001162 return
1163
1164 # Automatic selection based on issue number set for a current branch.
1165 # Rietveld takes precedence over Gerrit.
1166 assert not self.issue
1167 # Whether we find issue or not, we are doing the lookup.
1168 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001169 if self.GetBranch():
1170 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1171 issue = _git_get_branch_config_value(
1172 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1173 if issue:
1174 self._codereview = codereview
1175 self._codereview_impl = cls(self, **kwargs)
1176 self.issue = int(issue)
1177 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178
1179 # No issue is set for this branch, so decide based on repo-wide settings.
1180 return self._load_codereview_impl(
1181 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1182 **kwargs)
1183
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001184 def IsGerrit(self):
1185 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001186
1187 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001188 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001189
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001190 The return value is a string suitable for passing to git cl with the --cc
1191 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001192 """
1193 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001194 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001195 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001196 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1197 return self.cc
1198
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001199 def GetCCListWithoutDefault(self):
1200 """Return the users cc'd on this CL excluding default ones."""
1201 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001202 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001203 return self.cc
1204
Daniel Cheng7227d212017-11-17 08:12:37 -08001205 def ExtendCC(self, more_cc):
1206 """Extends the list of users to cc on this CL based on the changed files."""
1207 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208
1209 def GetBranch(self):
1210 """Returns the short branch name, e.g. 'master'."""
1211 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001212 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001213 if not branchref:
1214 return None
1215 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216 self.branch = ShortBranchName(self.branchref)
1217 return self.branch
1218
1219 def GetBranchRef(self):
1220 """Returns the full branch name, e.g. 'refs/heads/master'."""
1221 self.GetBranch() # Poke the lazy loader.
1222 return self.branchref
1223
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001224 def ClearBranch(self):
1225 """Clears cached branch data of this object."""
1226 self.branch = self.branchref = None
1227
tandrii5d48c322016-08-18 16:19:37 -07001228 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1229 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1230 kwargs['branch'] = self.GetBranch()
1231 return _git_get_branch_config_value(key, default, **kwargs)
1232
1233 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1234 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1235 assert self.GetBranch(), (
1236 'this CL must have an associated branch to %sset %s%s' %
1237 ('un' if value is None else '',
1238 key,
1239 '' if value is None else ' to %r' % value))
1240 kwargs['branch'] = self.GetBranch()
1241 return _git_set_branch_config_value(key, value, **kwargs)
1242
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001243 @staticmethod
1244 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001245 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 e.g. 'origin', 'refs/heads/master'
1247 """
1248 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001249 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1250
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001252 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001254 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1255 error_ok=True).strip()
1256 if upstream_branch:
1257 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001259 # Else, try to guess the origin remote.
1260 remote_branches = RunGit(['branch', '-r']).split()
1261 if 'origin/master' in remote_branches:
1262 # Fall back on origin/master if it exits.
1263 remote = 'origin'
1264 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001266 DieWithError(
1267 'Unable to determine default branch to diff against.\n'
1268 'Either pass complete "git diff"-style arguments, like\n'
1269 ' git cl upload origin/master\n'
1270 'or verify this branch is set up to track another \n'
1271 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272
1273 return remote, upstream_branch
1274
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001275 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001276 upstream_branch = self.GetUpstreamBranch()
1277 if not BranchExists(upstream_branch):
1278 DieWithError('The upstream for the current branch (%s) does not exist '
1279 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001280 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001281 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001282
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001283 def GetUpstreamBranch(self):
1284 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001285 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001287 upstream_branch = upstream_branch.replace('refs/heads/',
1288 'refs/remotes/%s/' % remote)
1289 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1290 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 self.upstream_branch = upstream_branch
1292 return self.upstream_branch
1293
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001294 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001295 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001296 remote, branch = None, self.GetBranch()
1297 seen_branches = set()
1298 while branch not in seen_branches:
1299 seen_branches.add(branch)
1300 remote, branch = self.FetchUpstreamTuple(branch)
1301 branch = ShortBranchName(branch)
1302 if remote != '.' or branch.startswith('refs/remotes'):
1303 break
1304 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001305 remotes = RunGit(['remote'], error_ok=True).split()
1306 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001307 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001308 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001309 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001310 logging.warn('Could not determine which remote this change is '
1311 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001312 else:
1313 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001314 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001315 branch = 'HEAD'
1316 if branch.startswith('refs/remotes'):
1317 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001318 elif branch.startswith('refs/branch-heads/'):
1319 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001320 else:
1321 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001322 return self._remote
1323
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001324 def GitSanityChecks(self, upstream_git_obj):
1325 """Checks git repo status and ensures diff is from local commits."""
1326
sbc@chromium.org79706062015-01-14 21:18:12 +00001327 if upstream_git_obj is None:
1328 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001329 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001330 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001331 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001332 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001333 return False
1334
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001335 # Verify the commit we're diffing against is in our current branch.
1336 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1337 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1338 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001339 print('ERROR: %s is not in the current branch. You may need to rebase '
1340 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001341 return False
1342
1343 # List the commits inside the diff, and verify they are all local.
1344 commits_in_diff = RunGit(
1345 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1346 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1347 remote_branch = remote_branch.strip()
1348 if code != 0:
1349 _, remote_branch = self.GetRemoteBranch()
1350
1351 commits_in_remote = RunGit(
1352 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1353
1354 common_commits = set(commits_in_diff) & set(commits_in_remote)
1355 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001356 print('ERROR: Your diff contains %d commits already in %s.\n'
1357 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1358 'the diff. If you are using a custom git flow, you can override'
1359 ' the reference used for this check with "git config '
1360 'gitcl.remotebranch <git-ref>".' % (
1361 len(common_commits), remote_branch, upstream_git_obj),
1362 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001363 return False
1364 return True
1365
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001366 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001367 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001368
1369 Returns None if it is not set.
1370 """
tandrii5d48c322016-08-18 16:19:37 -07001371 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001372
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373 def GetRemoteUrl(self):
1374 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1375
1376 Returns None if there is no remote.
1377 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001378 is_cached, value = self._cached_remote_url
1379 if is_cached:
1380 return value
1381
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001382 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001383 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1384
1385 # If URL is pointing to a local directory, it is probably a git cache.
1386 if os.path.isdir(url):
1387 url = RunGit(['config', 'remote.%s.url' % remote],
1388 error_ok=True,
1389 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001390 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001391 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001393 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001394 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001395 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001396 self.issue = self._GitGetBranchConfigValue(
1397 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001398 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 return self.issue
1400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 def GetIssueURL(self):
1402 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001403 issue = self.GetIssue()
1404 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001405 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001406 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001408 def GetDescription(self, pretty=False, force=False):
1409 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001411 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 self.has_description = True
1413 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001414 # Set width to 72 columns + 2 space indent.
1415 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001417 lines = self.description.splitlines()
1418 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 return self.description
1420
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001421 def GetDescriptionFooters(self):
1422 """Returns (non_footer_lines, footers) for the commit message.
1423
1424 Returns:
1425 non_footer_lines (list(str)) - Simple list of description lines without
1426 any footer. The lines do not contain newlines, nor does the list contain
1427 the empty line between the message and the footers.
1428 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1429 [("Change-Id", "Ideadbeef...."), ...]
1430 """
1431 raw_description = self.GetDescription()
1432 msg_lines, _, footers = git_footers.split_footers(raw_description)
1433 if footers:
1434 msg_lines = msg_lines[:len(msg_lines)-1]
1435 return msg_lines, footers
1436
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001438 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001439 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001440 self.patchset = self._GitGetBranchConfigValue(
1441 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001442 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 return self.patchset
1444
1445 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001446 """Set this branch's patchset. If patchset=0, clears the patchset."""
1447 assert self.GetBranch()
1448 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001449 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001450 else:
1451 self.patchset = int(patchset)
1452 self._GitSetBranchConfigValue(
1453 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001455 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001456 """Set this branch's issue. If issue isn't given, clears the issue."""
1457 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001459 issue = int(issue)
1460 self._GitSetBranchConfigValue(
1461 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001462 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001463 codereview_server = self._codereview_impl.GetCodereviewServer()
1464 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001465 self._GitSetBranchConfigValue(
1466 self._codereview_impl.CodereviewServerConfigKey(),
1467 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 else:
tandrii5d48c322016-08-18 16:19:37 -07001469 # Reset all of these just to be clean.
1470 reset_suffixes = [
1471 'last-upload-hash',
1472 self._codereview_impl.IssueConfigKey(),
1473 self._codereview_impl.PatchsetConfigKey(),
1474 self._codereview_impl.CodereviewServerConfigKey(),
1475 ] + self._PostUnsetIssueProperties()
1476 for prop in reset_suffixes:
1477 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001478 msg = RunGit(['log', '-1', '--format=%B']).strip()
1479 if msg and git_footers.get_footer_change_id(msg):
1480 print('WARNING: The change patched into this branch has a Change-Id. '
1481 'Removing it.')
1482 RunGit(['commit', '--amend', '-m',
1483 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001484 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001485 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001486
dnjba1b0f32016-09-02 12:37:42 -07001487 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001488 if not self.GitSanityChecks(upstream_branch):
1489 DieWithError('\nGit sanity check failure')
1490
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001491 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001492 if not root:
1493 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001494 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001495
1496 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001497 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001498 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001499 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001500 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001501 except subprocess2.CalledProcessError:
1502 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001503 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001504 'This branch probably doesn\'t exist anymore. To reset the\n'
1505 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001506 ' git branch --set-upstream-to origin/master %s\n'
1507 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001508 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001509
maruel@chromium.org52424302012-08-29 15:14:30 +00001510 issue = self.GetIssue()
1511 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001512 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001513 description = self.GetDescription()
1514 else:
1515 # If the change was never uploaded, use the log messages of all commits
1516 # up to the branch point, as git cl upload will prefill the description
1517 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001518 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1519 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001520
1521 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001522 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001523 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001524 name,
1525 description,
1526 absroot,
1527 files,
1528 issue,
1529 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001530 author,
1531 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001532
dsansomee2d6fd92016-09-08 00:10:47 -07001533 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001534 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001535 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001536 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001537
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001538 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1539 """Sets the description for this CL remotely.
1540
1541 You can get description_lines and footers with GetDescriptionFooters.
1542
1543 Args:
1544 description_lines (list(str)) - List of CL description lines without
1545 newline characters.
1546 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1547 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1548 `List-Of-Tokens`). It will be case-normalized so that each token is
1549 title-cased.
1550 """
1551 new_description = '\n'.join(description_lines)
1552 if footers:
1553 new_description += '\n'
1554 for k, v in footers:
1555 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1556 if not git_footers.FOOTER_PATTERN.match(foot):
1557 raise ValueError('Invalid footer %r' % foot)
1558 new_description += foot + '\n'
1559 self.UpdateDescription(new_description, force)
1560
Edward Lesmes8e282792018-04-03 18:50:29 -04001561 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001562 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1563 try:
1564 return presubmit_support.DoPresubmitChecks(change, committing,
1565 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1566 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001567 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1568 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001569 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001570 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001571
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001572 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1573 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001574 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1575 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001576 else:
1577 # Assume url.
1578 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1579 urlparse.urlparse(issue_arg))
1580 if not parsed_issue_arg or not parsed_issue_arg.valid:
1581 DieWithError('Failed to parse issue argument "%s". '
1582 'Must be an issue number or a valid URL.' % issue_arg)
1583 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001584 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001585
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001586 def CMDUpload(self, options, git_diff_args, orig_args):
1587 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001588 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001589 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001590 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001591 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001592 else:
1593 if self.GetBranch() is None:
1594 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1595
1596 # Default to diffing against common ancestor of upstream branch
1597 base_branch = self.GetCommonAncestorWithUpstream()
1598 git_diff_args = [base_branch, 'HEAD']
1599
Aaron Gablec4c40d12017-05-22 11:49:53 -07001600
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001601 # Fast best-effort checks to abort before running potentially
1602 # expensive hooks if uploading is likely to fail anyway. Passing these
1603 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001604 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001605 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001606
1607 # Apply watchlists on upload.
1608 change = self.GetChange(base_branch, None)
1609 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1610 files = [f.LocalPath() for f in change.AffectedFiles()]
1611 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001612 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001613
1614 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001615 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001616 # Set the reviewer list now so that presubmit checks can access it.
1617 change_description = ChangeDescription(change.FullDescriptionText())
1618 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001619 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001620 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001621 change)
1622 change.SetDescriptionText(change_description.description)
1623 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001624 may_prompt=not options.force,
1625 verbose=options.verbose,
1626 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001627 if not hook_results.should_continue():
1628 return 1
1629 if not options.reviewers and hook_results.reviewers:
1630 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001631 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001632
Aaron Gable13101a62018-02-09 13:20:41 -08001633 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001634 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001635 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001636 _git_set_branch_config_value('last-upload-hash',
1637 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001638 # Run post upload hooks, if specified.
1639 if settings.GetRunPostUploadHook():
1640 presubmit_support.DoPostUploadExecuter(
1641 change,
1642 self,
1643 settings.GetRoot(),
1644 options.verbose,
1645 sys.stdout)
1646
1647 # Upload all dependencies if specified.
1648 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001649 print()
1650 print('--dependencies has been specified.')
1651 print('All dependent local branches will be re-uploaded.')
1652 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001653 # Remove the dependencies flag from args so that we do not end up in a
1654 # loop.
1655 orig_args.remove('--dependencies')
1656 ret = upload_branch_deps(self, orig_args)
1657 return ret
1658
Ravi Mistry31e7d562018-04-02 12:53:57 -04001659 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1660 """Sets labels on the change based on the provided flags.
1661
1662 Sets labels if issue is already uploaded and known, else returns without
1663 doing anything.
1664
1665 Args:
1666 enable_auto_submit: Sets Auto-Submit+1 on the change.
1667 use_commit_queue: Sets Commit-Queue+2 on the change.
1668 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1669 both use_commit_queue and cq_dry_run are true.
1670 """
1671 if not self.GetIssue():
1672 return
1673 try:
1674 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1675 cq_dry_run)
1676 return 0
1677 except KeyboardInterrupt:
1678 raise
1679 except:
1680 labels = []
1681 if enable_auto_submit:
1682 labels.append('Auto-Submit')
1683 if use_commit_queue or cq_dry_run:
1684 labels.append('Commit-Queue')
1685 print('WARNING: Failed to set label(s) on your change: %s\n'
1686 'Either:\n'
1687 ' * Your project does not have the above label(s),\n'
1688 ' * You don\'t have permission to set the above label(s),\n'
1689 ' * There\'s a bug in this code (see stack trace below).\n' %
1690 (', '.join(labels)))
1691 # Still raise exception so that stack trace is printed.
1692 raise
1693
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001694 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001695 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001696
1697 Issue must have been already uploaded and known.
1698 """
1699 assert new_state in _CQState.ALL_STATES
1700 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001701 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001702 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001703 return 0
1704 except KeyboardInterrupt:
1705 raise
1706 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001707 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001708 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001709 ' * Your project has no CQ,\n'
1710 ' * You don\'t have permission to change the CQ state,\n'
1711 ' * There\'s a bug in this code (see stack trace below).\n'
1712 'Consider specifying which bots to trigger manually or asking your '
1713 'project owners for permissions or contacting Chrome Infra at:\n'
1714 'https://www.chromium.org/infra\n\n' %
1715 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001716 # Still raise exception so that stack trace is printed.
1717 raise
1718
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001719 # Forward methods to codereview specific implementation.
1720
Aaron Gable636b13f2017-07-14 10:42:48 -07001721 def AddComment(self, message, publish=None):
1722 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001723
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001724 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001725 """Returns list of _CommentSummary for each comment.
1726
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001727 args:
1728 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001729 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001730 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001731
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001732 def CloseIssue(self):
1733 return self._codereview_impl.CloseIssue()
1734
1735 def GetStatus(self):
1736 return self._codereview_impl.GetStatus()
1737
1738 def GetCodereviewServer(self):
1739 return self._codereview_impl.GetCodereviewServer()
1740
tandriide281ae2016-10-12 06:02:30 -07001741 def GetIssueOwner(self):
1742 """Get owner from codereview, which may differ from this checkout."""
1743 return self._codereview_impl.GetIssueOwner()
1744
Edward Lemur707d70b2018-02-07 00:50:14 +01001745 def GetReviewers(self):
1746 return self._codereview_impl.GetReviewers()
1747
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001748 def GetMostRecentPatchset(self):
1749 return self._codereview_impl.GetMostRecentPatchset()
1750
tandriide281ae2016-10-12 06:02:30 -07001751 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001752 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001753 return self._codereview_impl.CannotTriggerTryJobReason()
1754
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001755 def GetTryJobProperties(self, patchset=None):
1756 """Returns dictionary of properties to launch try job."""
1757 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001758
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001759 def __getattr__(self, attr):
1760 # This is because lots of untested code accesses Rietveld-specific stuff
1761 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001762 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001763 # Note that child method defines __getattr__ as well, and forwards it here,
1764 # because _RietveldChangelistImpl is not cleaned up yet, and given
1765 # deprecation of Rietveld, it should probably be just removed.
1766 # Until that time, avoid infinite recursion by bypassing __getattr__
1767 # of implementation class.
1768 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001769
1770
1771class _ChangelistCodereviewBase(object):
1772 """Abstract base class encapsulating codereview specifics of a changelist."""
1773 def __init__(self, changelist):
1774 self._changelist = changelist # instance of Changelist
1775
1776 def __getattr__(self, attr):
1777 # Forward methods to changelist.
1778 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1779 # _RietveldChangelistImpl to avoid this hack?
1780 return getattr(self._changelist, attr)
1781
1782 def GetStatus(self):
1783 """Apply a rough heuristic to give a simple summary of an issue's review
1784 or CQ status, assuming adherence to a common workflow.
1785
1786 Returns None if no issue for this branch, or specific string keywords.
1787 """
1788 raise NotImplementedError()
1789
1790 def GetCodereviewServer(self):
1791 """Returns server URL without end slash, like "https://codereview.com"."""
1792 raise NotImplementedError()
1793
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001794 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001795 """Fetches and returns description from the codereview server."""
1796 raise NotImplementedError()
1797
tandrii5d48c322016-08-18 16:19:37 -07001798 @classmethod
1799 def IssueConfigKey(cls):
1800 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001801 raise NotImplementedError()
1802
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001803 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001804 def PatchsetConfigKey(cls):
1805 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001806 raise NotImplementedError()
1807
tandrii5d48c322016-08-18 16:19:37 -07001808 @classmethod
1809 def CodereviewServerConfigKey(cls):
1810 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001811 raise NotImplementedError()
1812
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001813 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001814 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001815 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001816
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001817 def GetGerritObjForPresubmit(self):
1818 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1819 return None
1820
dsansomee2d6fd92016-09-08 00:10:47 -07001821 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001822 """Update the description on codereview site."""
1823 raise NotImplementedError()
1824
Aaron Gable636b13f2017-07-14 10:42:48 -07001825 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001826 """Posts a comment to the codereview site."""
1827 raise NotImplementedError()
1828
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001829 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001830 raise NotImplementedError()
1831
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001832 def CloseIssue(self):
1833 """Closes the issue."""
1834 raise NotImplementedError()
1835
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001836 def GetMostRecentPatchset(self):
1837 """Returns the most recent patchset number from the codereview site."""
1838 raise NotImplementedError()
1839
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001840 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001841 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001842 """Fetches and applies the issue.
1843
1844 Arguments:
1845 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1846 reject: if True, reject the failed patch instead of switching to 3-way
1847 merge. Rietveld only.
1848 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1849 only.
1850 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001851 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001852 """
1853 raise NotImplementedError()
1854
1855 @staticmethod
1856 def ParseIssueURL(parsed_url):
1857 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1858 failed."""
1859 raise NotImplementedError()
1860
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001861 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001862 """Best effort check that user is authenticated with codereview server.
1863
1864 Arguments:
1865 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001866 refresh: whether to attempt to refresh credentials. Ignored if not
1867 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001868 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001869 raise NotImplementedError()
1870
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001871 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001872 """Best effort check that uploading isn't supposed to fail for predictable
1873 reasons.
1874
1875 This method should raise informative exception if uploading shouldn't
1876 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001877
1878 Arguments:
1879 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001880 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001881 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001882
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001883 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001884 """Uploads a change to codereview."""
1885 raise NotImplementedError()
1886
Ravi Mistry31e7d562018-04-02 12:53:57 -04001887 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1888 """Sets labels on the change based on the provided flags.
1889
1890 Issue must have been already uploaded and known.
1891 """
1892 raise NotImplementedError()
1893
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001894 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001895 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001896
1897 Issue must have been already uploaded and known.
1898 """
1899 raise NotImplementedError()
1900
tandriie113dfd2016-10-11 10:20:12 -07001901 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001902 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001903 raise NotImplementedError()
1904
tandriide281ae2016-10-12 06:02:30 -07001905 def GetIssueOwner(self):
1906 raise NotImplementedError()
1907
Edward Lemur707d70b2018-02-07 00:50:14 +01001908 def GetReviewers(self):
1909 raise NotImplementedError()
1910
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001911 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001912 raise NotImplementedError()
1913
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001914
1915class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001916
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001917 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001918 super(_RietveldChangelistImpl, self).__init__(changelist)
1919 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001920 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001921 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001922
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001923 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001924 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001925 self._props = None
1926 self._rpc_server = None
1927
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001928 def GetCodereviewServer(self):
1929 if not self._rietveld_server:
1930 # If we're on a branch then get the server potentially associated
1931 # with that branch.
1932 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001933 self._rietveld_server = gclient_utils.UpgradeToHttps(
1934 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001935 if not self._rietveld_server:
1936 self._rietveld_server = settings.GetDefaultServerUrl()
1937 return self._rietveld_server
1938
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001939 def EnsureAuthenticated(self, force, refresh=False):
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001940 raise NotImplementedError
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001941
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001942 def EnsureCanUploadPatchset(self, force):
1943 # No checks for Rietveld because we are deprecating Rietveld.
1944 pass
1945
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001946 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001947 issue = self.GetIssue()
1948 assert issue
1949 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001950 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001951 except urllib2.HTTPError as e:
1952 if e.code == 404:
1953 DieWithError(
1954 ('\nWhile fetching the description for issue %d, received a '
1955 '404 (not found)\n'
1956 'error. It is likely that you deleted this '
1957 'issue on the server. If this is the\n'
1958 'case, please run\n\n'
1959 ' git cl issue 0\n\n'
1960 'to clear the association with the deleted issue. Then run '
1961 'this command again.') % issue)
1962 else:
1963 DieWithError(
1964 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1965 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001966 print('Warning: Failed to retrieve CL description due to network '
1967 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001968 return ''
1969
1970 def GetMostRecentPatchset(self):
1971 return self.GetIssueProperties()['patchsets'][-1]
1972
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001973 def GetIssueProperties(self):
1974 if self._props is None:
1975 issue = self.GetIssue()
1976 if not issue:
1977 self._props = {}
1978 else:
1979 self._props = self.RpcServer().get_issue_properties(issue, True)
1980 return self._props
1981
tandriie113dfd2016-10-11 10:20:12 -07001982 def CannotTriggerTryJobReason(self):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00001983 raise NotImplementedError()
tandriie113dfd2016-10-11 10:20:12 -07001984
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001985 def GetTryJobProperties(self, patchset=None):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00001986 raise NotImplementedError()
tandrii8c5a3532016-11-04 07:52:02 -07001987
tandriide281ae2016-10-12 06:02:30 -07001988 def GetIssueOwner(self):
1989 return (self.GetIssueProperties() or {}).get('owner_email')
1990
Edward Lemur707d70b2018-02-07 00:50:14 +01001991 def GetReviewers(self):
1992 return (self.GetIssueProperties() or {}).get('reviewers')
1993
Aaron Gable636b13f2017-07-14 10:42:48 -07001994 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001995 return self.RpcServer().add_comment(self.GetIssue(), message)
1996
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001997 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001998 summary = []
1999 for message in self.GetIssueProperties().get('messages', []):
2000 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2001 summary.append(_CommentSummary(
2002 date=date,
2003 disapproval=bool(message['disapproval']),
2004 approval=bool(message['approval']),
2005 sender=message['sender'],
2006 message=message['text'],
2007 ))
2008 return summary
2009
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002010 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002011 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002012 or CQ status, assuming adherence to a common workflow.
2013
2014 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002015 * 'error' - error from review tool (including deleted issues)
2016 * 'unsent' - not sent for review
2017 * 'waiting' - waiting for review
2018 * 'reply' - waiting for owner to reply to review
2019 * 'not lgtm' - Code-Review label has been set negatively
2020 * 'lgtm' - LGTM from at least one approved reviewer
2021 * 'commit' - in the commit queue
2022 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002023 """
2024 if not self.GetIssue():
2025 return None
2026
2027 try:
2028 props = self.GetIssueProperties()
2029 except urllib2.HTTPError:
2030 return 'error'
2031
2032 if props.get('closed'):
2033 # Issue is closed.
2034 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002035 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002036 # Issue is in the commit queue.
2037 return 'commit'
2038
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002039 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002040 if not messages:
2041 # No message was sent.
2042 return 'unsent'
2043
2044 if get_approving_reviewers(props):
2045 return 'lgtm'
2046 elif get_approving_reviewers(props, disapproval=True):
2047 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002048
tandrii9d2c7a32016-06-22 03:42:45 -07002049 # Skip CQ messages that don't require owner's action.
2050 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2051 if 'Dry run:' in messages[-1]['text']:
2052 messages.pop()
2053 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2054 # This message always follows prior messages from CQ,
2055 # so skip this too.
2056 messages.pop()
2057 else:
2058 # This is probably a CQ messages warranting user attention.
2059 break
2060
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002061 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002062 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002063 return 'reply'
2064 return 'waiting'
2065
dsansomee2d6fd92016-09-08 00:10:47 -07002066 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002067 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002068
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002069 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002070 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002071
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002072 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002073 return self.SetFlags({flag: value})
2074
2075 def SetFlags(self, flags):
2076 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002077 """
phajdan.jr68598232016-08-10 03:28:28 -07002078 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002079 try:
tandrii4b233bd2016-07-06 03:50:29 -07002080 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002081 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002082 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002083 if e.code == 404:
2084 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2085 if e.code == 403:
2086 DieWithError(
2087 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002088 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002089 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002090
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002091 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002092 """Returns an upload.RpcServer() to access this review's rietveld instance.
2093 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002094 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002095 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002096 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002097 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002098 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002099
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002100 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002101 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002102 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002103
tandrii5d48c322016-08-18 16:19:37 -07002104 @classmethod
2105 def PatchsetConfigKey(cls):
2106 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002107
tandrii5d48c322016-08-18 16:19:37 -07002108 @classmethod
2109 def CodereviewServerConfigKey(cls):
2110 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002111
Ravi Mistry31e7d562018-04-02 12:53:57 -04002112 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2113 raise NotImplementedError()
2114
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002115 def SetCQState(self, new_state):
2116 props = self.GetIssueProperties()
2117 if props.get('private'):
2118 DieWithError('Cannot set-commit on private issue')
2119
2120 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002121 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002122 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002123 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002124 else:
tandrii4b233bd2016-07-06 03:50:29 -07002125 assert new_state == _CQState.DRY_RUN
2126 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002127
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002128 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002129 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002130 # PatchIssue should never be called with a dirty tree. It is up to the
2131 # caller to check this, but just in case we assert here since the
2132 # consequences of the caller not checking this could be dire.
2133 assert(not git_common.is_dirty_git_tree('apply'))
2134 assert(parsed_issue_arg.valid)
2135 self._changelist.issue = parsed_issue_arg.issue
2136 if parsed_issue_arg.hostname:
2137 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2138
skobes6468b902016-10-24 08:45:10 -07002139 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2140 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2141 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002142 try:
skobes6468b902016-10-24 08:45:10 -07002143 scm_obj.apply_patch(patchset_object)
2144 except Exception as e:
2145 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002146 return 1
2147
2148 # If we had an issue, commit the current state and register the issue.
2149 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002150 self.SetIssue(self.GetIssue())
2151 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002152 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2153 'patch from issue %(i)s at patchset '
2154 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2155 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002156 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002157 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002158 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002159 return 0
2160
2161 @staticmethod
2162 def ParseIssueURL(parsed_url):
2163 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2164 return None
wychen3c1c1722016-08-04 11:46:36 -07002165 # Rietveld patch: https://domain/<number>/#ps<patchset>
2166 match = re.match(r'/(\d+)/$', parsed_url.path)
2167 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2168 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002169 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002170 issue=int(match.group(1)),
2171 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002172 hostname=parsed_url.netloc,
2173 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002174 # Typical url: https://domain/<issue_number>[/[other]]
2175 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2176 if match:
skobes6468b902016-10-24 08:45:10 -07002177 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002178 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002179 hostname=parsed_url.netloc,
2180 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002181 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2182 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2183 if match:
skobes6468b902016-10-24 08:45:10 -07002184 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002185 issue=int(match.group(1)),
2186 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002187 hostname=parsed_url.netloc,
2188 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002189 return None
2190
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002191 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002192 """Upload the patch to Rietveld."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00002193 raise NotImplementedError
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002194
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002195
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002196class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002197 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002198 # auth_config is Rietveld thing, kept here to preserve interface only.
2199 super(_GerritChangelistImpl, self).__init__(changelist)
2200 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002201 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002202 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002203 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002204 # Map from change number (issue) to its detail cache.
2205 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002206
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002207 if codereview_host is not None:
2208 assert not codereview_host.startswith('https://'), codereview_host
2209 self._gerrit_host = codereview_host
2210 self._gerrit_server = 'https://%s' % codereview_host
2211
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002212 def _GetGerritHost(self):
2213 # Lazy load of configs.
2214 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002215 if self._gerrit_host and '.' not in self._gerrit_host:
2216 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2217 # This happens for internal stuff http://crbug.com/614312.
2218 parsed = urlparse.urlparse(self.GetRemoteUrl())
2219 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002220 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002221 ' Your current remote is: %s' % self.GetRemoteUrl())
2222 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2223 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002224 return self._gerrit_host
2225
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002226 def _GetGitHost(self):
2227 """Returns git host to be used when uploading change to Gerrit."""
2228 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2229
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002230 def GetCodereviewServer(self):
2231 if not self._gerrit_server:
2232 # If we're on a branch then get the server potentially associated
2233 # with that branch.
2234 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002235 self._gerrit_server = self._GitGetBranchConfigValue(
2236 self.CodereviewServerConfigKey())
2237 if self._gerrit_server:
2238 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002239 if not self._gerrit_server:
2240 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2241 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002242 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002243 parts[0] = parts[0] + '-review'
2244 self._gerrit_host = '.'.join(parts)
2245 self._gerrit_server = 'https://%s' % self._gerrit_host
2246 return self._gerrit_server
2247
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002248 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002249 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002250 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002251 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002252 logging.warn('can\'t detect Gerrit project.')
2253 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002254 project = urlparse.urlparse(remote_url).path.strip('/')
2255 if project.endswith('.git'):
2256 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00002257 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
2258 # 'a/' prefix, because 'a/' prefix is used to force authentication in
2259 # gitiles/git-over-https protocol. E.g.,
2260 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
2261 # as
2262 # https://chromium.googlesource.com/v8/v8
2263 if project.startswith('a/'):
2264 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002265 return project
2266
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002267 def _GerritChangeIdentifier(self):
2268 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2269
2270 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002271 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002272 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002273 project = self._GetGerritProject()
2274 if project:
2275 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2276 # Fall back on still unique, but less efficient change number.
2277 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002278
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002279 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002280 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002281 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002282
tandrii5d48c322016-08-18 16:19:37 -07002283 @classmethod
2284 def PatchsetConfigKey(cls):
2285 return 'gerritpatchset'
2286
2287 @classmethod
2288 def CodereviewServerConfigKey(cls):
2289 return 'gerritserver'
2290
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002291 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002292 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002293 if settings.GetGerritSkipEnsureAuthenticated():
2294 # For projects with unusual authentication schemes.
2295 # See http://crbug.com/603378.
2296 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002297
2298 # Check presence of cookies only if using cookies-based auth method.
2299 cookie_auth = gerrit_util.Authenticator.get()
2300 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002301 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002302
2303 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002304 self.GetCodereviewServer()
2305 git_host = self._GetGitHost()
2306 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002307
2308 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2309 git_auth = cookie_auth.get_auth_header(git_host)
2310 if gerrit_auth and git_auth:
2311 if gerrit_auth == git_auth:
2312 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002313 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002314 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002315 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002316 ' %s\n'
2317 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002318 ' Consider running the following command:\n'
2319 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002320 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002321 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002322 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002323 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002324 cookie_auth.get_new_password_message(git_host)))
2325 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002326 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002327 return
2328 else:
2329 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002330 ([] if gerrit_auth else [self._gerrit_host]) +
2331 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002332 DieWithError('Credentials for the following hosts are required:\n'
2333 ' %s\n'
2334 'These are read from %s (or legacy %s)\n'
2335 '%s' % (
2336 '\n '.join(missing),
2337 cookie_auth.get_gitcookies_path(),
2338 cookie_auth.get_netrc_path(),
2339 cookie_auth.get_new_password_message(git_host)))
2340
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002341 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002342 if not self.GetIssue():
2343 return
2344
2345 # Warm change details cache now to avoid RPCs later, reducing latency for
2346 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002347 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002348 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002349
2350 status = self._GetChangeDetail()['status']
2351 if status in ('MERGED', 'ABANDONED'):
2352 DieWithError('Change %s has been %s, new uploads are not allowed' %
2353 (self.GetIssueURL(),
2354 'submitted' if status == 'MERGED' else 'abandoned'))
2355
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002356 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2357 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2358 # Apparently this check is not very important? Otherwise get_auth_email
2359 # could have been added to other implementations of Authenticator.
2360 cookies_auth = gerrit_util.Authenticator.get()
2361 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002362 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002363
2364 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002365 if self.GetIssueOwner() == cookies_user:
2366 return
2367 logging.debug('change %s owner is %s, cookies user is %s',
2368 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002369 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002370 # so ask what Gerrit thinks of this user.
2371 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2372 if details['email'] == self.GetIssueOwner():
2373 return
2374 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002375 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002376 'as %s.\n'
2377 'Uploading may fail due to lack of permissions.' %
2378 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2379 confirm_or_exit(action='upload')
2380
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002381 def _PostUnsetIssueProperties(self):
2382 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002383 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002384
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002385 def GetGerritObjForPresubmit(self):
2386 return presubmit_support.GerritAccessor(self._GetGerritHost())
2387
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002388 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002389 """Apply a rough heuristic to give a simple summary of an issue's review
2390 or CQ status, assuming adherence to a common workflow.
2391
2392 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002393 * 'error' - error from review tool (including deleted issues)
2394 * 'unsent' - no reviewers added
2395 * 'waiting' - waiting for review
2396 * 'reply' - waiting for uploader to reply to review
2397 * 'lgtm' - Code-Review label has been set
2398 * 'commit' - in the commit queue
2399 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002400 """
2401 if not self.GetIssue():
2402 return None
2403
2404 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002405 data = self._GetChangeDetail([
2406 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002407 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002408 return 'error'
2409
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002410 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002411 return 'closed'
2412
Aaron Gable9ab38c62017-04-06 14:36:33 -07002413 if data['labels'].get('Commit-Queue', {}).get('approved'):
2414 # The section will have an "approved" subsection if anyone has voted
2415 # the maximum value on the label.
2416 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002417
Aaron Gable9ab38c62017-04-06 14:36:33 -07002418 if data['labels'].get('Code-Review', {}).get('approved'):
2419 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002420
2421 if not data.get('reviewers', {}).get('REVIEWER', []):
2422 return 'unsent'
2423
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002424 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002425 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2426 last_message_author = messages.pop().get('author', {})
2427 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002428 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2429 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002430 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002431 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002432 if last_message_author.get('_account_id') == owner:
2433 # Most recent message was by owner.
2434 return 'waiting'
2435 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002436 # Some reply from non-owner.
2437 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002438
2439 # Somehow there are no messages even though there are reviewers.
2440 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002441
2442 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002443 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002444 patchset = data['revisions'][data['current_revision']]['_number']
2445 self.SetPatchset(patchset)
2446 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002447
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002448 def FetchDescription(self, force=False):
2449 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2450 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002451 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002452 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002453
dsansomee2d6fd92016-09-08 00:10:47 -07002454 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002455 if gerrit_util.HasPendingChangeEdit(
2456 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002457 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002458 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002459 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002460 'unpublished edit. Either publish the edit in the Gerrit web UI '
2461 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002462
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002463 gerrit_util.DeletePendingChangeEdit(
2464 self._GetGerritHost(), self._GerritChangeIdentifier())
2465 gerrit_util.SetCommitMessage(
2466 self._GetGerritHost(), self._GerritChangeIdentifier(),
2467 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002468
Aaron Gable636b13f2017-07-14 10:42:48 -07002469 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002470 gerrit_util.SetReview(
2471 self._GetGerritHost(), self._GerritChangeIdentifier(),
2472 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002473
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002474 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002475 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002476 messages = self._GetChangeDetail(
2477 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2478 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002479 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002480
2481 # Build dictionary of file comments for easy access and sorting later.
2482 # {author+date: {path: {patchset: {line: url+message}}}}
2483 comments = collections.defaultdict(
2484 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2485 for path, line_comments in file_comments.iteritems():
2486 for comment in line_comments:
2487 if comment.get('tag', '').startswith('autogenerated'):
2488 continue
2489 key = (comment['author']['email'], comment['updated'])
2490 if comment.get('side', 'REVISION') == 'PARENT':
2491 patchset = 'Base'
2492 else:
2493 patchset = 'PS%d' % comment['patch_set']
2494 line = comment.get('line', 0)
2495 url = ('https://%s/c/%s/%s/%s#%s%s' %
2496 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2497 'b' if comment.get('side') == 'PARENT' else '',
2498 str(line) if line else ''))
2499 comments[key][path][patchset][line] = (url, comment['message'])
2500
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002501 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002502 for msg in messages:
2503 # Don't bother showing autogenerated messages.
2504 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2505 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002506 # Gerrit spits out nanoseconds.
2507 assert len(msg['date'].split('.')[-1]) == 9
2508 date = datetime.datetime.strptime(msg['date'][:-3],
2509 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002510 message = msg['message']
2511 key = (msg['author']['email'], msg['date'])
2512 if key in comments:
2513 message += '\n'
2514 for path, patchsets in sorted(comments.get(key, {}).items()):
2515 if readable:
2516 message += '\n%s' % path
2517 for patchset, lines in sorted(patchsets.items()):
2518 for line, (url, content) in sorted(lines.items()):
2519 if line:
2520 line_str = 'Line %d' % line
2521 path_str = '%s:%d:' % (path, line)
2522 else:
2523 line_str = 'File comment'
2524 path_str = '%s:0:' % path
2525 if readable:
2526 message += '\n %s, %s: %s' % (patchset, line_str, url)
2527 message += '\n %s\n' % content
2528 else:
2529 message += '\n%s ' % path_str
2530 message += '\n%s\n' % content
2531
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002532 summary.append(_CommentSummary(
2533 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002534 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002535 sender=msg['author']['email'],
2536 # These could be inferred from the text messages and correlated with
2537 # Code-Review label maximum, however this is not reliable.
2538 # Leaving as is until the need arises.
2539 approval=False,
2540 disapproval=False,
2541 ))
2542 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002543
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002544 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002545 gerrit_util.AbandonChange(
2546 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002547
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002548 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002549 gerrit_util.SubmitChange(
2550 self._GetGerritHost(), self._GerritChangeIdentifier(),
2551 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002552
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002553 def _GetChangeDetail(self, options=None, no_cache=False):
2554 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002555
2556 If fresh data is needed, set no_cache=True which will clear cache and
2557 thus new data will be fetched from Gerrit.
2558 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002559 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002560 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002561
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002562 # Optimization to avoid multiple RPCs:
2563 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2564 'CURRENT_COMMIT' not in options):
2565 options.append('CURRENT_COMMIT')
2566
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002567 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002568 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002569 options = [o.upper() for o in options]
2570
2571 # Check in cache first unless no_cache is True.
2572 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002573 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002574 else:
2575 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002576 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002577 # Assumption: data fetched before with extra options is suitable
2578 # for return for a smaller set of options.
2579 # For example, if we cached data for
2580 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2581 # and request is for options=[CURRENT_REVISION],
2582 # THEN we can return prior cached data.
2583 if options_set.issubset(cached_options_set):
2584 return data
2585
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002586 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002587 data = gerrit_util.GetChangeDetail(
2588 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002589 except gerrit_util.GerritError as e:
2590 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002591 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002592 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002593
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002594 self._detail_cache.setdefault(cache_key, []).append(
2595 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002596 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002597
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002598 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002599 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002600 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002601 data = gerrit_util.GetChangeCommit(
2602 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002603 except gerrit_util.GerritError as e:
2604 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002605 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002606 raise
agable32978d92016-11-01 12:55:02 -07002607 return data
2608
Olivier Robin75ee7252018-04-13 10:02:56 +02002609 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002610 if git_common.is_dirty_git_tree('land'):
2611 return 1
tandriid60367b2016-06-22 05:25:12 -07002612 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2613 if u'Commit-Queue' in detail.get('labels', {}):
2614 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002615 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2616 'which can test and land changes for you. '
2617 'Are you sure you wish to bypass it?\n',
2618 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002619
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002620 differs = True
tandriic4344b52016-08-29 06:04:54 -07002621 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002622 # Note: git diff outputs nothing if there is no diff.
2623 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002624 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002625 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002626 if detail['current_revision'] == last_upload:
2627 differs = False
2628 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002629 print('WARNING: Local branch contents differ from latest uploaded '
2630 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002631 if differs:
2632 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002633 confirm_or_exit(
2634 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2635 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002636 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002637 elif not bypass_hooks:
2638 hook_results = self.RunHook(
2639 committing=True,
2640 may_prompt=not force,
2641 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002642 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2643 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002644 if not hook_results.should_continue():
2645 return 1
2646
2647 self.SubmitIssue(wait_for_merge=True)
2648 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002649 links = self._GetChangeCommit().get('web_links', [])
2650 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002651 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002652 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002653 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002654 return 0
2655
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002656 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002657 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002658 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002659 assert not directory
2660 assert parsed_issue_arg.valid
2661
2662 self._changelist.issue = parsed_issue_arg.issue
2663
2664 if parsed_issue_arg.hostname:
2665 self._gerrit_host = parsed_issue_arg.hostname
2666 self._gerrit_server = 'https://%s' % self._gerrit_host
2667
tandriic2405f52016-10-10 08:13:15 -07002668 try:
2669 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002670 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002671 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002672
2673 if not parsed_issue_arg.patchset:
2674 # Use current revision by default.
2675 revision_info = detail['revisions'][detail['current_revision']]
2676 patchset = int(revision_info['_number'])
2677 else:
2678 patchset = parsed_issue_arg.patchset
2679 for revision_info in detail['revisions'].itervalues():
2680 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2681 break
2682 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002683 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002684 (parsed_issue_arg.patchset, self.GetIssue()))
2685
Aaron Gable697a91b2018-01-19 15:20:15 -08002686 remote_url = self._changelist.GetRemoteUrl()
2687 if remote_url.endswith('.git'):
2688 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002689 remote_url = remote_url.rstrip('/')
2690
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002691 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002692 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002693
2694 if remote_url != fetch_info['url']:
2695 DieWithError('Trying to patch a change from %s but this repo appears '
2696 'to be %s.' % (fetch_info['url'], remote_url))
2697
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002698 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002699
Aaron Gable62619a32017-06-16 08:22:09 -07002700 if force:
2701 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2702 print('Checked out commit for change %i patchset %i locally' %
2703 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002704 elif nocommit:
2705 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2706 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002707 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002708 RunGit(['cherry-pick', 'FETCH_HEAD'])
2709 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002710 (parsed_issue_arg.issue, patchset))
2711 print('Note: this created a local commit which does not have '
2712 'the same hash as the one uploaded for review. This will make '
2713 'uploading changes based on top of this branch difficult.\n'
2714 'If you want to do that, use "git cl patch --force" instead.')
2715
Stefan Zagerd08043c2017-10-12 12:07:02 -07002716 if self.GetBranch():
2717 self.SetIssue(parsed_issue_arg.issue)
2718 self.SetPatchset(patchset)
2719 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2720 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2721 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2722 else:
2723 print('WARNING: You are in detached HEAD state.\n'
2724 'The patch has been applied to your checkout, but you will not be '
2725 'able to upload a new patch set to the gerrit issue.\n'
2726 'Try using the \'-b\' option if you would like to work on a '
2727 'branch and/or upload a new patch set.')
2728
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002729 return 0
2730
2731 @staticmethod
2732 def ParseIssueURL(parsed_url):
2733 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2734 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002735 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2736 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002737 # Short urls like https://domain/<issue_number> can be used, but don't allow
2738 # specifying the patchset (you'd 404), but we allow that here.
2739 if parsed_url.path == '/':
2740 part = parsed_url.fragment
2741 else:
2742 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002743 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002744 if match:
2745 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002746 issue=int(match.group(3)),
2747 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002748 hostname=parsed_url.netloc,
2749 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002750 return None
2751
tandrii16e0b4e2016-06-07 10:34:28 -07002752 def _GerritCommitMsgHookCheck(self, offer_removal):
2753 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2754 if not os.path.exists(hook):
2755 return
2756 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2757 # custom developer made one.
2758 data = gclient_utils.FileRead(hook)
2759 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2760 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002761 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002762 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002763 'and may interfere with it in subtle ways.\n'
2764 'We recommend you remove the commit-msg hook.')
2765 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002766 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002767 gclient_utils.rm_file_or_tree(hook)
2768 print('Gerrit commit-msg hook removed.')
2769 else:
2770 print('OK, will keep Gerrit commit-msg hook in place.')
2771
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002772 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002773 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002774 if options.squash and options.no_squash:
2775 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002776
2777 if not options.squash and not options.no_squash:
2778 # Load default for user, repo, squash=true, in this order.
2779 options.squash = settings.GetSquashGerritUploads()
2780 elif options.no_squash:
2781 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002782
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002783 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002784 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002785
Aaron Gableb56ad332017-01-06 15:24:31 -08002786 # This may be None; default fallback value is determined in logic below.
2787 title = options.title
2788
Dominic Battre7d1c4842017-10-27 09:17:28 +02002789 # Extract bug number from branch name.
2790 bug = options.bug
2791 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2792 if not bug and match:
2793 bug = match.group(1)
2794
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002795 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002796 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002797 if self.GetIssue():
2798 # Try to get the message from a previous upload.
2799 message = self.GetDescription()
2800 if not message:
2801 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002802 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002803 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002804 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002805 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002806 # When uploading a subsequent patchset, -m|--message is taken
2807 # as the patchset title if --title was not provided.
2808 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002809 else:
2810 default_title = RunGit(
2811 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002812 if options.force:
2813 title = default_title
2814 else:
2815 title = ask_for_data(
2816 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002817 change_id = self._GetChangeDetail()['change_id']
2818 while True:
2819 footer_change_ids = git_footers.get_footer_change_id(message)
2820 if footer_change_ids == [change_id]:
2821 break
2822 if not footer_change_ids:
2823 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002824 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002825 continue
2826 # There is already a valid footer but with different or several ids.
2827 # Doing this automatically is non-trivial as we don't want to lose
2828 # existing other footers, yet we want to append just 1 desired
2829 # Change-Id. Thus, just create a new footer, but let user verify the
2830 # new description.
2831 message = '%s\n\nChange-Id: %s' % (message, change_id)
2832 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002833 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002834 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002835 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002836 'Please, check the proposed correction to the description, '
2837 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2838 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2839 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002840 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002841 if not options.force:
2842 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002843 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002844 message = change_desc.description
2845 if not message:
2846 DieWithError("Description is empty. Aborting...")
2847 # Continue the while loop.
2848 # Sanity check of this code - we should end up with proper message
2849 # footer.
2850 assert [change_id] == git_footers.get_footer_change_id(message)
2851 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002852 else: # if not self.GetIssue()
2853 if options.message:
2854 message = options.message
2855 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002856 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002857 if options.title:
2858 message = options.title + '\n\n' + message
2859 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002860
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002861 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002862 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002863 # On first upload, patchset title is always this string, while
2864 # --title flag gets converted to first line of message.
2865 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002866 if not change_desc.description:
2867 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002868 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002869 if len(change_ids) > 1:
2870 DieWithError('too many Change-Id footers, at most 1 allowed.')
2871 if not change_ids:
2872 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002873 change_desc.set_description(git_footers.add_footer_change_id(
2874 change_desc.description,
2875 GenerateGerritChangeId(change_desc.description)))
2876 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002877 assert len(change_ids) == 1
2878 change_id = change_ids[0]
2879
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002880 if options.reviewers or options.tbrs or options.add_owners_to:
2881 change_desc.update_reviewers(options.reviewers, options.tbrs,
2882 options.add_owners_to, change)
2883
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002884 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002885 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2886 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002887 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002888 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2889 desc_tempfile.write(change_desc.description)
2890 desc_tempfile.close()
2891 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2892 '-F', desc_tempfile.name]).strip()
2893 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002894 else:
2895 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002896 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002897 if not change_desc.description:
2898 DieWithError("Description is empty. Aborting...")
2899
2900 if not git_footers.get_footer_change_id(change_desc.description):
2901 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002902 change_desc.set_description(
2903 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002904 if options.reviewers or options.tbrs or options.add_owners_to:
2905 change_desc.update_reviewers(options.reviewers, options.tbrs,
2906 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002907 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002908 # For no-squash mode, we assume the remote called "origin" is the one we
2909 # want. It is not worthwhile to support different workflows for
2910 # no-squash mode.
2911 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002912 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2913
2914 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002915 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002916 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2917 ref_to_push)]).splitlines()
2918 if len(commits) > 1:
2919 print('WARNING: This will upload %d commits. Run the following command '
2920 'to see which commits will be uploaded: ' % len(commits))
2921 print('git log %s..%s' % (parent, ref_to_push))
2922 print('You can also use `git squash-branch` to squash these into a '
2923 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002924 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002925
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002926 if options.reviewers or options.tbrs or options.add_owners_to:
2927 change_desc.update_reviewers(options.reviewers, options.tbrs,
2928 options.add_owners_to, change)
2929
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002930 reviewers = sorted(change_desc.get_reviewers())
2931 # Add cc's from the CC_LIST and --cc flag (if any).
2932 if not options.private and not options.no_autocc:
2933 cc = self.GetCCList().split(',')
2934 else:
2935 cc = []
2936 if options.cc:
2937 cc.extend(options.cc)
2938 cc = filter(None, [email.strip() for email in cc])
2939 if change_desc.get_cced():
2940 cc.extend(change_desc.get_cced())
Andrii Shyshkalovba7b0a42018-10-15 03:20:35 +00002941 valid_accounts = gerrit_util.ValidAccounts(
2942 self._GetGerritHost(), reviewers + cc)
2943 logging.debug('accounts %s are valid, %s invalid', sorted(valid_accounts),
2944 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002945
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002946 # Extra options that can be specified at push time. Doc:
2947 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002948 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002949
Aaron Gable844cf292017-06-28 11:32:59 -07002950 # By default, new changes are started in WIP mode, and subsequent patchsets
2951 # don't send email. At any time, passing --send-mail will mark the change
2952 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002953 if options.send_mail:
2954 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002955 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002956 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002957 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002958 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002959 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002960
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002961 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002962 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002963
Aaron Gable9b713dd2016-12-14 16:04:21 -08002964 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002965 # Punctuation and whitespace in |title| must be percent-encoded.
2966 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002967
agablec6787972016-09-09 16:13:34 -07002968 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002969 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002970
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002971 for r in sorted(reviewers):
2972 if r in valid_accounts:
2973 refspec_opts.append('r=%s' % r)
2974 reviewers.remove(r)
2975 else:
2976 # TODO(tandrii): this should probably be a hard failure.
2977 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2978 % r)
2979 for c in sorted(cc):
2980 # refspec option will be rejected if cc doesn't correspond to an
2981 # account, even though REST call to add such arbitrary cc may succeed.
2982 if c in valid_accounts:
2983 refspec_opts.append('cc=%s' % c)
2984 cc.remove(c)
2985
2986
rmistry9eadede2016-09-19 11:22:43 -07002987 if options.topic:
2988 # Documentation on Gerrit topics is here:
2989 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002990 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002991
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002992 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002993 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002994 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002995 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002996 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2997
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002998 refspec_suffix = ''
2999 if refspec_opts:
3000 refspec_suffix = '%' + ','.join(refspec_opts)
3001 assert ' ' not in refspec_suffix, (
3002 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3003 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3004
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003005 try:
Edward Lemur83bd7f42018-10-10 00:14:21 +00003006 # TODO(crbug.com/881860): Remove.
Edward Lemur47faa062018-10-11 19:46:02 +00003007 # Clear the log after each git-cl upload run by setting mode='w'.
3008 handler = logging.FileHandler(gerrit_util.GERRIT_ERR_LOG_FILE, mode='w')
3009 handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
3010
3011 GERRIT_ERR_LOGGER.addHandler(handler)
3012 GERRIT_ERR_LOGGER.setLevel(logging.INFO)
3013 # Don't propagate to root logger, so that logs are not printed.
3014 GERRIT_ERR_LOGGER.propagate = 0
3015
Edward Lemur83bd7f42018-10-10 00:14:21 +00003016 # Get interesting headers from git push, to be displayed to the user if
3017 # subsequent Gerrit RPC calls fail.
3018 env = os.environ.copy()
3019 env['GIT_CURL_VERBOSE'] = '1'
3020 class FilterHeaders(object):
3021 """Filter git push headers and store them in a file.
3022
3023 Regular git push output is printed directly.
3024 """
3025
3026 def __init__(self):
3027 # The output from git push that we want to store in a file.
3028 self._output = ''
3029 # Keeps track of whether the current line is part of a request header.
3030 self._on_header = False
3031 # Keeps track of repeated empty lines, which mark the end of a request
3032 # header.
3033 self._last_line_empty = False
3034
3035 def __call__(self, line):
3036 """Handle a single line of git push output."""
3037 if not line:
3038 # Two consecutive empty lines mark the end of a header.
3039 if self._last_line_empty:
3040 self._on_header = False
3041 self._last_line_empty = True
3042 return
3043
3044 self._last_line_empty = False
3045 # A line starting with '>' marks the beggining of a request header.
3046 if line[0] == '>':
3047 self._on_header = True
3048 GERRIT_ERR_LOGGER.info(line)
3049 # Lines not starting with '*' or '<', and not part of a request header
3050 # should be displayed to the user.
3051 elif line[0] not in '*<' and not self._on_header:
3052 print(line)
3053 # Flush after every line: useful for seeing progress when running as
3054 # recipe.
3055 sys.stdout.flush()
3056 # Filter out the cookie and authorization headers.
3057 elif ('cookie: ' not in line.lower()
3058 and 'authorization: ' not in line.lower()):
3059 GERRIT_ERR_LOGGER.info(line)
3060
3061 filter_fn = FilterHeaders()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003062 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00003063 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemur83bd7f42018-10-10 00:14:21 +00003064 print_stdout=False,
3065 filter_fn=filter_fn,
3066 env=env)
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003067 except subprocess2.CalledProcessError:
3068 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003069 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003070 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003071 'credential problems:\n'
3072 ' git cl creds-check\n',
3073 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003074
3075 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003076 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003077 change_numbers = [m.group(1)
3078 for m in map(regex.match, push_stdout.splitlines())
3079 if m]
3080 if len(change_numbers) != 1:
3081 DieWithError(
3082 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003083 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003084 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003085 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003086
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00003087 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003088 # GetIssue() is not set in case of non-squash uploads according to tests.
3089 # TODO(agable): non-squash uploads in git cl should be removed.
3090 gerrit_util.AddReviewers(
3091 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003092 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003093 reviewers, cc,
3094 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003095
Aaron Gablefd238082017-06-07 13:42:34 -07003096 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003097 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3098 score = 1
3099 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3100 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3101 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003102 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003103 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003104 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003105 msg='Self-approving for TBR',
3106 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003107
Andrii Shyshkalovdd788442018-10-13 17:55:29 +00003108 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
3109 options.cq_dry_run)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003110 return 0
3111
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003112 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3113 change_desc):
3114 """Computes parent of the generated commit to be uploaded to Gerrit.
3115
3116 Returns revision or a ref name.
3117 """
3118 if custom_cl_base:
3119 # Try to avoid creating additional unintended CLs when uploading, unless
3120 # user wants to take this risk.
3121 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3122 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3123 local_ref_of_target_remote])
3124 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003125 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003126 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3127 'If you proceed with upload, more than 1 CL may be created by '
3128 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3129 'If you are certain that specified base `%s` has already been '
3130 'uploaded to Gerrit as another CL, you may proceed.\n' %
3131 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3132 if not force:
3133 confirm_or_exit(
3134 'Do you take responsibility for cleaning up potential mess '
3135 'resulting from proceeding with upload?',
3136 action='upload')
3137 return custom_cl_base
3138
Aaron Gablef97e33d2017-03-30 15:44:27 -07003139 if remote != '.':
3140 return self.GetCommonAncestorWithUpstream()
3141
3142 # If our upstream branch is local, we base our squashed commit on its
3143 # squashed version.
3144 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3145
Aaron Gablef97e33d2017-03-30 15:44:27 -07003146 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003147 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003148
3149 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003150 # TODO(tandrii): consider checking parent change in Gerrit and using its
3151 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3152 # the tree hash of the parent branch. The upside is less likely bogus
3153 # requests to reupload parent change just because it's uploadhash is
3154 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003155 parent = RunGit(['config',
3156 'branch.%s.gerritsquashhash' % upstream_branch_name],
3157 error_ok=True).strip()
3158 # Verify that the upstream branch has been uploaded too, otherwise
3159 # Gerrit will create additional CLs when uploading.
3160 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3161 RunGitSilent(['rev-parse', parent + ':'])):
3162 DieWithError(
3163 '\nUpload upstream branch %s first.\n'
3164 'It is likely that this branch has been rebased since its last '
3165 'upload, so you just need to upload it again.\n'
3166 '(If you uploaded it with --no-squash, then branch dependencies '
3167 'are not supported, and you should reupload with --squash.)'
3168 % upstream_branch_name,
3169 change_desc)
3170 return parent
3171
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003172 def _AddChangeIdToCommitMessage(self, options, args):
3173 """Re-commits using the current message, assumes the commit hook is in
3174 place.
3175 """
3176 log_desc = options.message or CreateDescriptionFromLog(args)
3177 git_command = ['commit', '--amend', '-m', log_desc]
3178 RunGit(git_command)
3179 new_log_desc = CreateDescriptionFromLog(args)
3180 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003181 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003182 return new_log_desc
3183 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003184 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003185
Ravi Mistry31e7d562018-04-02 12:53:57 -04003186 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3187 """Sets labels on the change based on the provided flags."""
3188 labels = {}
3189 notify = None;
3190 if enable_auto_submit:
3191 labels['Auto-Submit'] = 1
3192 if use_commit_queue:
3193 labels['Commit-Queue'] = 2
3194 elif cq_dry_run:
3195 labels['Commit-Queue'] = 1
3196 notify = False
3197 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003198 gerrit_util.SetReview(
3199 self._GetGerritHost(),
3200 self._GerritChangeIdentifier(),
3201 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003202
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003203 def SetCQState(self, new_state):
3204 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003205 vote_map = {
3206 _CQState.NONE: 0,
3207 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003208 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003209 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003210 labels = {'Commit-Queue': vote_map[new_state]}
3211 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003212 gerrit_util.SetReview(
3213 self._GetGerritHost(), self._GerritChangeIdentifier(),
3214 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003215
tandriie113dfd2016-10-11 10:20:12 -07003216 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003217 try:
3218 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003219 except GerritChangeNotExists:
3220 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003221
3222 if data['status'] in ('ABANDONED', 'MERGED'):
3223 return 'CL %s is closed' % self.GetIssue()
3224
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003225 def GetTryJobProperties(self, patchset=None):
3226 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003227 data = self._GetChangeDetail(['ALL_REVISIONS'])
3228 patchset = int(patchset or self.GetPatchset())
3229 assert patchset
3230 revision_data = None # Pylint wants it to be defined.
3231 for revision_data in data['revisions'].itervalues():
3232 if int(revision_data['_number']) == patchset:
3233 break
3234 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003235 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003236 (patchset, self.GetIssue()))
3237 return {
3238 'patch_issue': self.GetIssue(),
3239 'patch_set': patchset or self.GetPatchset(),
3240 'patch_project': data['project'],
3241 'patch_storage': 'gerrit',
3242 'patch_ref': revision_data['fetch']['http']['ref'],
3243 'patch_repository_url': revision_data['fetch']['http']['url'],
3244 'patch_gerrit_url': self.GetCodereviewServer(),
3245 }
tandriie113dfd2016-10-11 10:20:12 -07003246
tandriide281ae2016-10-12 06:02:30 -07003247 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003248 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003249
Edward Lemur707d70b2018-02-07 00:50:14 +01003250 def GetReviewers(self):
3251 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3252 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3253
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003254
3255_CODEREVIEW_IMPLEMENTATIONS = {
3256 'rietveld': _RietveldChangelistImpl,
3257 'gerrit': _GerritChangelistImpl,
3258}
3259
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003260
iannuccie53c9352016-08-17 14:40:40 -07003261def _add_codereview_issue_select_options(parser, extra=""):
3262 _add_codereview_select_options(parser)
3263
3264 text = ('Operate on this issue number instead of the current branch\'s '
3265 'implicit issue.')
3266 if extra:
3267 text += ' '+extra
3268 parser.add_option('-i', '--issue', type=int, help=text)
3269
3270
3271def _process_codereview_issue_select_options(parser, options):
3272 _process_codereview_select_options(parser, options)
3273 if options.issue is not None and not options.forced_codereview:
3274 parser.error('--issue must be specified with either --rietveld or --gerrit')
3275
3276
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003277def _add_codereview_select_options(parser):
3278 """Appends --gerrit and --rietveld options to force specific codereview."""
3279 parser.codereview_group = optparse.OptionGroup(
3280 parser, 'EXPERIMENTAL! Codereview override options')
3281 parser.add_option_group(parser.codereview_group)
3282 parser.codereview_group.add_option(
3283 '--gerrit', action='store_true',
3284 help='Force the use of Gerrit for codereview')
3285 parser.codereview_group.add_option(
3286 '--rietveld', action='store_true',
3287 help='Force the use of Rietveld for codereview')
3288
3289
3290def _process_codereview_select_options(parser, options):
3291 if options.gerrit and options.rietveld:
3292 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3293 options.forced_codereview = None
3294 if options.gerrit:
3295 options.forced_codereview = 'gerrit'
3296 elif options.rietveld:
3297 options.forced_codereview = 'rietveld'
3298
3299
tandriif9aefb72016-07-01 09:06:51 -07003300def _get_bug_line_values(default_project, bugs):
3301 """Given default_project and comma separated list of bugs, yields bug line
3302 values.
3303
3304 Each bug can be either:
3305 * a number, which is combined with default_project
3306 * string, which is left as is.
3307
3308 This function may produce more than one line, because bugdroid expects one
3309 project per line.
3310
3311 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3312 ['v8:123', 'chromium:789']
3313 """
3314 default_bugs = []
3315 others = []
3316 for bug in bugs.split(','):
3317 bug = bug.strip()
3318 if bug:
3319 try:
3320 default_bugs.append(int(bug))
3321 except ValueError:
3322 others.append(bug)
3323
3324 if default_bugs:
3325 default_bugs = ','.join(map(str, default_bugs))
3326 if default_project:
3327 yield '%s:%s' % (default_project, default_bugs)
3328 else:
3329 yield default_bugs
3330 for other in sorted(others):
3331 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3332 yield other
3333
3334
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003335class ChangeDescription(object):
3336 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003337 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003338 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003339 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003340 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003341 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3342 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3343 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3344 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003345
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003346 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003347 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003348
agable@chromium.org42c20792013-09-12 17:34:49 +00003349 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003350 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003351 return '\n'.join(self._description_lines)
3352
3353 def set_description(self, desc):
3354 if isinstance(desc, basestring):
3355 lines = desc.splitlines()
3356 else:
3357 lines = [line.rstrip() for line in desc]
3358 while lines and not lines[0]:
3359 lines.pop(0)
3360 while lines and not lines[-1]:
3361 lines.pop(-1)
3362 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003363
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003364 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3365 """Rewrites the R=/TBR= line(s) as a single line each.
3366
3367 Args:
3368 reviewers (list(str)) - list of additional emails to use for reviewers.
3369 tbrs (list(str)) - list of additional emails to use for TBRs.
3370 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3371 the change that are missing OWNER coverage. If this is not None, you
3372 must also pass a value for `change`.
3373 change (Change) - The Change that should be used for OWNERS lookups.
3374 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003375 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003376 assert isinstance(tbrs, list), tbrs
3377
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003378 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003379 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003380
3381 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003382 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003383
3384 reviewers = set(reviewers)
3385 tbrs = set(tbrs)
3386 LOOKUP = {
3387 'TBR': tbrs,
3388 'R': reviewers,
3389 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003390
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003391 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003392 regexp = re.compile(self.R_LINE)
3393 matches = [regexp.match(line) for line in self._description_lines]
3394 new_desc = [l for i, l in enumerate(self._description_lines)
3395 if not matches[i]]
3396 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003397
agable@chromium.org42c20792013-09-12 17:34:49 +00003398 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003399
3400 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003401 for match in matches:
3402 if not match:
3403 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003404 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3405
3406 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003407 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003408 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003409 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003410 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003411 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003412 LOOKUP[add_owners_to].update(
3413 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003414
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003415 # If any folks ended up in both groups, remove them from tbrs.
3416 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003417
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003418 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3419 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003420
3421 # Put the new lines in the description where the old first R= line was.
3422 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3423 if 0 <= line_loc < len(self._description_lines):
3424 if new_tbr_line:
3425 self._description_lines.insert(line_loc, new_tbr_line)
3426 if new_r_line:
3427 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003428 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003429 if new_r_line:
3430 self.append_footer(new_r_line)
3431 if new_tbr_line:
3432 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003433
Aaron Gable3a16ed12017-03-23 10:51:55 -07003434 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003435 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003436 self.set_description([
3437 '# Enter a description of the change.',
3438 '# This will be displayed on the codereview site.',
3439 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003440 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003441 '--------------------',
3442 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003443
agable@chromium.org42c20792013-09-12 17:34:49 +00003444 regexp = re.compile(self.BUG_LINE)
3445 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003446 prefix = settings.GetBugPrefix()
3447 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003448 if git_footer:
3449 self.append_footer('Bug: %s' % ', '.join(values))
3450 else:
3451 for value in values:
3452 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003453
agable@chromium.org42c20792013-09-12 17:34:49 +00003454 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003455 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003456 if not content:
3457 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003458 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003459
Bruce Dawson2377b012018-01-11 16:46:49 -08003460 # Strip off comments and default inserted "Bug:" line.
3461 clean_lines = [line.rstrip() for line in lines if not
3462 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003463 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003464 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003465 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003466
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003467 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003468 """Adds a footer line to the description.
3469
3470 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3471 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3472 that Gerrit footers are always at the end.
3473 """
3474 parsed_footer_line = git_footers.parse_footer(line)
3475 if parsed_footer_line:
3476 # Line is a gerrit footer in the form: Footer-Key: any value.
3477 # Thus, must be appended observing Gerrit footer rules.
3478 self.set_description(
3479 git_footers.add_footer(self.description,
3480 key=parsed_footer_line[0],
3481 value=parsed_footer_line[1]))
3482 return
3483
3484 if not self._description_lines:
3485 self._description_lines.append(line)
3486 return
3487
3488 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3489 if gerrit_footers:
3490 # git_footers.split_footers ensures that there is an empty line before
3491 # actual (gerrit) footers, if any. We have to keep it that way.
3492 assert top_lines and top_lines[-1] == ''
3493 top_lines, separator = top_lines[:-1], top_lines[-1:]
3494 else:
3495 separator = [] # No need for separator if there are no gerrit_footers.
3496
3497 prev_line = top_lines[-1] if top_lines else ''
3498 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3499 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3500 top_lines.append('')
3501 top_lines.append(line)
3502 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003503
tandrii99a72f22016-08-17 14:33:24 -07003504 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003505 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003506 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003507 reviewers = [match.group(2).strip()
3508 for match in matches
3509 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003510 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003511
bradnelsond975b302016-10-23 12:20:23 -07003512 def get_cced(self):
3513 """Retrieves the list of reviewers."""
3514 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3515 cced = [match.group(2).strip() for match in matches if match]
3516 return cleanup_list(cced)
3517
Nodir Turakulov23b82142017-11-16 11:04:25 -08003518 def get_hash_tags(self):
3519 """Extracts and sanitizes a list of Gerrit hashtags."""
3520 subject = (self._description_lines or ('',))[0]
3521 subject = re.sub(
3522 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3523
3524 tags = []
3525 start = 0
3526 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3527 while True:
3528 m = bracket_exp.match(subject, start)
3529 if not m:
3530 break
3531 tags.append(self.sanitize_hash_tag(m.group(1)))
3532 start = m.end()
3533
3534 if not tags:
3535 # Try "Tag: " prefix.
3536 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3537 if m:
3538 tags.append(self.sanitize_hash_tag(m.group(1)))
3539 return tags
3540
3541 @classmethod
3542 def sanitize_hash_tag(cls, tag):
3543 """Returns a sanitized Gerrit hash tag.
3544
3545 A sanitized hashtag can be used as a git push refspec parameter value.
3546 """
3547 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3548
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003549 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3550 """Updates this commit description given the parent.
3551
3552 This is essentially what Gnumbd used to do.
3553 Consult https://goo.gl/WMmpDe for more details.
3554 """
3555 assert parent_msg # No, orphan branch creation isn't supported.
3556 assert parent_hash
3557 assert dest_ref
3558 parent_footer_map = git_footers.parse_footers(parent_msg)
3559 # This will also happily parse svn-position, which GnumbD is no longer
3560 # supporting. While we'd generate correct footers, the verifier plugin
3561 # installed in Gerrit will block such commit (ie git push below will fail).
3562 parent_position = git_footers.get_position(parent_footer_map)
3563
3564 # Cherry-picks may have last line obscuring their prior footers,
3565 # from git_footers perspective. This is also what Gnumbd did.
3566 cp_line = None
3567 if (self._description_lines and
3568 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3569 cp_line = self._description_lines.pop()
3570
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003571 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003572
3573 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3574 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003575 for i, line in enumerate(footer_lines):
3576 k, v = git_footers.parse_footer(line) or (None, None)
3577 if k and k.startswith('Cr-'):
3578 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003579
3580 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003581 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003582 if parent_position[0] == dest_ref:
3583 # Same branch as parent.
3584 number = int(parent_position[1]) + 1
3585 else:
3586 number = 1 # New branch, and extra lineage.
3587 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3588 int(parent_position[1])))
3589
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003590 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3591 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003592
3593 self._description_lines = top_lines
3594 if cp_line:
3595 self._description_lines.append(cp_line)
3596 if self._description_lines[-1] != '':
3597 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003598 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003599
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003600
Aaron Gablea1bab272017-04-11 16:38:18 -07003601def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003602 """Retrieves the reviewers that approved a CL from the issue properties with
3603 messages.
3604
3605 Note that the list may contain reviewers that are not committer, thus are not
3606 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003607
3608 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003609 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003610 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003611 return sorted(
3612 set(
3613 message['sender']
3614 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003615 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003616 )
3617 )
3618
3619
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003620def FindCodereviewSettingsFile(filename='codereview.settings'):
3621 """Finds the given file starting in the cwd and going up.
3622
3623 Only looks up to the top of the repository unless an
3624 'inherit-review-settings-ok' file exists in the root of the repository.
3625 """
3626 inherit_ok_file = 'inherit-review-settings-ok'
3627 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003628 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003629 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3630 root = '/'
3631 while True:
3632 if filename in os.listdir(cwd):
3633 if os.path.isfile(os.path.join(cwd, filename)):
3634 return open(os.path.join(cwd, filename))
3635 if cwd == root:
3636 break
3637 cwd = os.path.dirname(cwd)
3638
3639
3640def LoadCodereviewSettingsFromFile(fileobj):
3641 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003642 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003643
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003644 def SetProperty(name, setting, unset_error_ok=False):
3645 fullname = 'rietveld.' + name
3646 if setting in keyvals:
3647 RunGit(['config', fullname, keyvals[setting]])
3648 else:
3649 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3650
tandrii48df5812016-10-17 03:55:37 -07003651 if not keyvals.get('GERRIT_HOST', False):
3652 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003653 # Only server setting is required. Other settings can be absent.
3654 # In that case, we ignore errors raised during option deletion attempt.
3655 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003656 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003657 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3658 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003659 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003660 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3661 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003662 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003663 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3664 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003666 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003667 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003668
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003669 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003670 RunGit(['config', 'gerrit.squash-uploads',
3671 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003672
tandrii@chromium.org28253532016-04-14 13:46:56 +00003673 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003674 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003675 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3676
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003677 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003678 # should be of the form
3679 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3680 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003681 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3682 keyvals['ORIGIN_URL_CONFIG']])
3683
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003684
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003685def urlretrieve(source, destination):
3686 """urllib is broken for SSL connections via a proxy therefore we
3687 can't use urllib.urlretrieve()."""
3688 with open(destination, 'w') as f:
3689 f.write(urllib2.urlopen(source).read())
3690
3691
ukai@chromium.org712d6102013-11-27 00:52:58 +00003692def hasSheBang(fname):
3693 """Checks fname is a #! script."""
3694 with open(fname) as f:
3695 return f.read(2).startswith('#!')
3696
3697
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003698# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3699def DownloadHooks(*args, **kwargs):
3700 pass
3701
3702
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003703def DownloadGerritHook(force):
3704 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003705
3706 Args:
3707 force: True to update hooks. False to install hooks if not present.
3708 """
3709 if not settings.GetIsGerrit():
3710 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003711 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003712 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3713 if not os.access(dst, os.X_OK):
3714 if os.path.exists(dst):
3715 if not force:
3716 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003717 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003718 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003719 if not hasSheBang(dst):
3720 DieWithError('Not a script: %s\n'
3721 'You need to download from\n%s\n'
3722 'into .git/hooks/commit-msg and '
3723 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003724 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3725 except Exception:
3726 if os.path.exists(dst):
3727 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003728 DieWithError('\nFailed to download hooks.\n'
3729 'You need to download from\n%s\n'
3730 'into .git/hooks/commit-msg and '
3731 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003732
3733
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003734def GetRietveldCodereviewSettingsInteractively():
3735 """Prompt the user for settings."""
3736 server = settings.GetDefaultServerUrl(error_ok=True)
3737 prompt = 'Rietveld server (host[:port])'
3738 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3739 newserver = ask_for_data(prompt + ':')
3740 if not server and not newserver:
3741 newserver = DEFAULT_SERVER
3742 if newserver:
3743 newserver = gclient_utils.UpgradeToHttps(newserver)
3744 if newserver != server:
3745 RunGit(['config', 'rietveld.server', newserver])
3746
3747 def SetProperty(initial, caption, name, is_url):
3748 prompt = caption
3749 if initial:
3750 prompt += ' ("x" to clear) [%s]' % initial
3751 new_val = ask_for_data(prompt + ':')
3752 if new_val == 'x':
3753 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3754 elif new_val:
3755 if is_url:
3756 new_val = gclient_utils.UpgradeToHttps(new_val)
3757 if new_val != initial:
3758 RunGit(['config', 'rietveld.' + name, new_val])
3759
3760 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3761 SetProperty(settings.GetDefaultPrivateFlag(),
3762 'Private flag (rietveld only)', 'private', False)
3763 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3764 'tree-status-url', False)
3765 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3766 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3767 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3768 'run-post-upload-hook', False)
3769
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003770
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003771class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003772 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003773
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003774 _GOOGLESOURCE = 'googlesource.com'
3775
3776 def __init__(self):
3777 # Cached list of [host, identity, source], where source is either
3778 # .gitcookies or .netrc.
3779 self._all_hosts = None
3780
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003781 def ensure_configured_gitcookies(self):
3782 """Runs checks and suggests fixes to make git use .gitcookies from default
3783 path."""
3784 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3785 configured_path = RunGitSilent(
3786 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003787 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003788 if configured_path:
3789 self._ensure_default_gitcookies_path(configured_path, default)
3790 else:
3791 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003792
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003793 @staticmethod
3794 def _ensure_default_gitcookies_path(configured_path, default_path):
3795 assert configured_path
3796 if configured_path == default_path:
3797 print('git is already configured to use your .gitcookies from %s' %
3798 configured_path)
3799 return
3800
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003801 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003802 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3803 (configured_path, default_path))
3804
3805 if not os.path.exists(configured_path):
3806 print('However, your configured .gitcookies file is missing.')
3807 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3808 action='reconfigure')
3809 RunGit(['config', '--global', 'http.cookiefile', default_path])
3810 return
3811
3812 if os.path.exists(default_path):
3813 print('WARNING: default .gitcookies file already exists %s' %
3814 default_path)
3815 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3816 default_path)
3817
3818 confirm_or_exit('Move existing .gitcookies to default location?',
3819 action='move')
3820 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003821 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003822 print('Moved and reconfigured git to use .gitcookies from %s' %
3823 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003824
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003825 @staticmethod
3826 def _configure_gitcookies_path(default_path):
3827 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3828 if os.path.exists(netrc_path):
3829 print('You seem to be using outdated .netrc for git credentials: %s' %
3830 netrc_path)
3831 print('This tool will guide you through setting up recommended '
3832 '.gitcookies store for git credentials.\n'
3833 '\n'
3834 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3835 ' git config --global --unset http.cookiefile\n'
3836 ' mv %s %s.backup\n\n' % (default_path, default_path))
3837 confirm_or_exit(action='setup .gitcookies')
3838 RunGit(['config', '--global', 'http.cookiefile', default_path])
3839 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003840
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003841 def get_hosts_with_creds(self, include_netrc=False):
3842 if self._all_hosts is None:
3843 a = gerrit_util.CookiesAuthenticator()
3844 self._all_hosts = [
3845 (h, u, s)
3846 for h, u, s in itertools.chain(
3847 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3848 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3849 )
3850 if h.endswith(self._GOOGLESOURCE)
3851 ]
3852
3853 if include_netrc:
3854 return self._all_hosts
3855 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3856
3857 def print_current_creds(self, include_netrc=False):
3858 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3859 if not hosts:
3860 print('No Git/Gerrit credentials found')
3861 return
3862 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3863 header = [('Host', 'User', 'Which file'),
3864 ['=' * l for l in lengths]]
3865 for row in (header + hosts):
3866 print('\t'.join((('%%+%ds' % l) % s)
3867 for l, s in zip(lengths, row)))
3868
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003869 @staticmethod
3870 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003871 """Parses identity "git-<username>.domain" into <username> and domain."""
3872 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003873 # distinguishable from sub-domains. But we do know typical domains:
3874 if identity.endswith('.chromium.org'):
3875 domain = 'chromium.org'
3876 username = identity[:-len('.chromium.org')]
3877 else:
3878 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003879 if username.startswith('git-'):
3880 username = username[len('git-'):]
3881 return username, domain
3882
3883 def _get_usernames_of_domain(self, domain):
3884 """Returns list of usernames referenced by .gitcookies in a given domain."""
3885 identities_by_domain = {}
3886 for _, identity, _ in self.get_hosts_with_creds():
3887 username, domain = self._parse_identity(identity)
3888 identities_by_domain.setdefault(domain, []).append(username)
3889 return identities_by_domain.get(domain)
3890
3891 def _canonical_git_googlesource_host(self, host):
3892 """Normalizes Gerrit hosts (with '-review') to Git host."""
3893 assert host.endswith(self._GOOGLESOURCE)
3894 # Prefix doesn't include '.' at the end.
3895 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3896 if prefix.endswith('-review'):
3897 prefix = prefix[:-len('-review')]
3898 return prefix + '.' + self._GOOGLESOURCE
3899
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003900 def _canonical_gerrit_googlesource_host(self, host):
3901 git_host = self._canonical_git_googlesource_host(host)
3902 prefix = git_host.split('.', 1)[0]
3903 return prefix + '-review.' + self._GOOGLESOURCE
3904
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003905 def _get_counterpart_host(self, host):
3906 assert host.endswith(self._GOOGLESOURCE)
3907 git = self._canonical_git_googlesource_host(host)
3908 gerrit = self._canonical_gerrit_googlesource_host(git)
3909 return git if gerrit == host else gerrit
3910
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003911 def has_generic_host(self):
3912 """Returns whether generic .googlesource.com has been configured.
3913
3914 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3915 """
3916 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3917 if host == '.' + self._GOOGLESOURCE:
3918 return True
3919 return False
3920
3921 def _get_git_gerrit_identity_pairs(self):
3922 """Returns map from canonic host to pair of identities (Git, Gerrit).
3923
3924 One of identities might be None, meaning not configured.
3925 """
3926 host_to_identity_pairs = {}
3927 for host, identity, _ in self.get_hosts_with_creds():
3928 canonical = self._canonical_git_googlesource_host(host)
3929 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3930 idx = 0 if canonical == host else 1
3931 pair[idx] = identity
3932 return host_to_identity_pairs
3933
3934 def get_partially_configured_hosts(self):
3935 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003936 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3937 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3938 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003939
3940 def get_conflicting_hosts(self):
3941 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003942 host
3943 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003944 if None not in (i1, i2) and i1 != i2)
3945
3946 def get_duplicated_hosts(self):
3947 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3948 return set(host for host, count in counters.iteritems() if count > 1)
3949
3950 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3951 'chromium.googlesource.com': 'chromium.org',
3952 'chrome-internal.googlesource.com': 'google.com',
3953 }
3954
3955 def get_hosts_with_wrong_identities(self):
3956 """Finds hosts which **likely** reference wrong identities.
3957
3958 Note: skips hosts which have conflicting identities for Git and Gerrit.
3959 """
3960 hosts = set()
3961 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3962 pair = self._get_git_gerrit_identity_pairs().get(host)
3963 if pair and pair[0] == pair[1]:
3964 _, domain = self._parse_identity(pair[0])
3965 if domain != expected:
3966 hosts.add(host)
3967 return hosts
3968
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003969 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003970 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003971 hosts = sorted(hosts)
3972 assert hosts
3973 if extra_column_func is None:
3974 extras = [''] * len(hosts)
3975 else:
3976 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003977 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3978 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003979 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003980 lines.append(tmpl % he)
3981 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003982
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003983 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003984 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003985 yield ('.googlesource.com wildcard record detected',
3986 ['Chrome Infrastructure team recommends to list full host names '
3987 'explicitly.'],
3988 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003989
3990 dups = self.get_duplicated_hosts()
3991 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003992 yield ('The following hosts were defined twice',
3993 self._format_hosts(dups),
3994 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003995
3996 partial = self.get_partially_configured_hosts()
3997 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003998 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3999 'These hosts are missing',
4000 self._format_hosts(partial, lambda host: 'but %s defined' %
4001 self._get_counterpart_host(host)),
4002 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004003
4004 conflicting = self.get_conflicting_hosts()
4005 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004006 yield ('The following Git hosts have differing credentials from their '
4007 'Gerrit counterparts',
4008 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4009 tuple(self._get_git_gerrit_identity_pairs()[host])),
4010 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004011
4012 wrong = self.get_hosts_with_wrong_identities()
4013 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004014 yield ('These hosts likely use wrong identity',
4015 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4016 (self._get_git_gerrit_identity_pairs()[host][0],
4017 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4018 wrong)
4019
4020 def find_and_report_problems(self):
4021 """Returns True if there was at least one problem, else False."""
4022 found = False
4023 bad_hosts = set()
4024 for title, sublines, hosts in self._find_problems():
4025 if not found:
4026 found = True
4027 print('\n\n.gitcookies problem report:\n')
4028 bad_hosts.update(hosts or [])
4029 print(' %s%s' % (title , (':' if sublines else '')))
4030 if sublines:
4031 print()
4032 print(' %s' % '\n '.join(sublines))
4033 print()
4034
4035 if bad_hosts:
4036 assert found
4037 print(' You can manually remove corresponding lines in your %s file and '
4038 'visit the following URLs with correct account to generate '
4039 'correct credential lines:\n' %
4040 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4041 print(' %s' % '\n '.join(sorted(set(
4042 gerrit_util.CookiesAuthenticator().get_new_password_url(
4043 self._canonical_git_googlesource_host(host))
4044 for host in bad_hosts
4045 ))))
4046 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004047
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004048
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004049@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004050def CMDcreds_check(parser, args):
4051 """Checks credentials and suggests changes."""
4052 _, _ = parser.parse_args(args)
4053
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004054 # Code below checks .gitcookies. Abort if using something else.
4055 authn = gerrit_util.Authenticator.get()
4056 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
4057 if isinstance(authn, gerrit_util.GceAuthenticator):
4058 DieWithError(
4059 'This command is not designed for GCE, are you on a bot?\n'
4060 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
4061 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004062 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004063 'This command is not designed for bot environment. It checks '
4064 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004065
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004066 checker = _GitCookiesChecker()
4067 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004068
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004069 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004070 checker.print_current_creds(include_netrc=True)
4071
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004072 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004073 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004074 return 0
4075 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004076
4077
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004078@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004079@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004080def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004081 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004082
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004083 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004084 # TODO(tandrii): remove this once we switch to Gerrit.
4085 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004086 parser.add_option('--activate-update', action='store_true',
4087 help='activate auto-updating [rietveld] section in '
4088 '.git/config')
4089 parser.add_option('--deactivate-update', action='store_true',
4090 help='deactivate auto-updating [rietveld] section in '
4091 '.git/config')
4092 options, args = parser.parse_args(args)
4093
4094 if options.deactivate_update:
4095 RunGit(['config', 'rietveld.autoupdate', 'false'])
4096 return
4097
4098 if options.activate_update:
4099 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4100 return
4101
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004102 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004103 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004104 return 0
4105
4106 url = args[0]
4107 if not url.endswith('codereview.settings'):
4108 url = os.path.join(url, 'codereview.settings')
4109
4110 # Load code review settings and download hooks (if available).
4111 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4112 return 0
4113
4114
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004115@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004116def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004117 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004118 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4119 branch = ShortBranchName(branchref)
4120 _, args = parser.parse_args(args)
4121 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004122 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004123 return RunGit(['config', 'branch.%s.base-url' % branch],
4124 error_ok=False).strip()
4125 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004126 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004127 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4128 error_ok=False).strip()
4129
4130
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004131def color_for_status(status):
4132 """Maps a Changelist status to color, for CMDstatus and other tools."""
4133 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004134 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004135 'waiting': Fore.BLUE,
4136 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004137 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004138 'lgtm': Fore.GREEN,
4139 'commit': Fore.MAGENTA,
4140 'closed': Fore.CYAN,
4141 'error': Fore.WHITE,
4142 }.get(status, Fore.WHITE)
4143
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004144
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004145def get_cl_statuses(changes, fine_grained, max_processes=None):
4146 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004147
4148 If fine_grained is true, this will fetch CL statuses from the server.
4149 Otherwise, simply indicate if there's a matching url for the given branches.
4150
4151 If max_processes is specified, it is used as the maximum number of processes
4152 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4153 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004154
4155 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004156 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004157 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004158 upload.verbosity = 0
4159
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004160 if not changes:
4161 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004162
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004163 if not fine_grained:
4164 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004165 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004166 for cl in changes:
4167 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004168 return
4169
4170 # First, sort out authentication issues.
4171 logging.debug('ensuring credentials exist')
4172 for cl in changes:
4173 cl.EnsureAuthenticated(force=False, refresh=True)
4174
4175 def fetch(cl):
4176 try:
4177 return (cl, cl.GetStatus())
4178 except:
4179 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004180 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004181 raise
4182
4183 threads_count = len(changes)
4184 if max_processes:
4185 threads_count = max(1, min(threads_count, max_processes))
4186 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4187
4188 pool = ThreadPool(threads_count)
4189 fetched_cls = set()
4190 try:
4191 it = pool.imap_unordered(fetch, changes).__iter__()
4192 while True:
4193 try:
4194 cl, status = it.next(timeout=5)
4195 except multiprocessing.TimeoutError:
4196 break
4197 fetched_cls.add(cl)
4198 yield cl, status
4199 finally:
4200 pool.close()
4201
4202 # Add any branches that failed to fetch.
4203 for cl in set(changes) - fetched_cls:
4204 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004205
rmistry@google.com2dd99862015-06-22 12:22:18 +00004206
4207def upload_branch_deps(cl, args):
4208 """Uploads CLs of local branches that are dependents of the current branch.
4209
4210 If the local branch dependency tree looks like:
4211 test1 -> test2.1 -> test3.1
4212 -> test3.2
4213 -> test2.2 -> test3.3
4214
4215 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4216 run on the dependent branches in this order:
4217 test2.1, test3.1, test3.2, test2.2, test3.3
4218
4219 Note: This function does not rebase your local dependent branches. Use it when
4220 you make a change to the parent branch that will not conflict with its
4221 dependent branches, and you would like their dependencies updated in
4222 Rietveld.
4223 """
4224 if git_common.is_dirty_git_tree('upload-branch-deps'):
4225 return 1
4226
4227 root_branch = cl.GetBranch()
4228 if root_branch is None:
4229 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4230 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004231 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00004232 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4233 'patchset dependencies without an uploaded CL.')
4234
4235 branches = RunGit(['for-each-ref',
4236 '--format=%(refname:short) %(upstream:short)',
4237 'refs/heads'])
4238 if not branches:
4239 print('No local branches found.')
4240 return 0
4241
4242 # Create a dictionary of all local branches to the branches that are dependent
4243 # on it.
4244 tracked_to_dependents = collections.defaultdict(list)
4245 for b in branches.splitlines():
4246 tokens = b.split()
4247 if len(tokens) == 2:
4248 branch_name, tracked = tokens
4249 tracked_to_dependents[tracked].append(branch_name)
4250
vapiera7fbd5a2016-06-16 09:17:49 -07004251 print()
4252 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004253 dependents = []
4254 def traverse_dependents_preorder(branch, padding=''):
4255 dependents_to_process = tracked_to_dependents.get(branch, [])
4256 padding += ' '
4257 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004258 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004259 dependents.append(dependent)
4260 traverse_dependents_preorder(dependent, padding)
4261 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004262 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004263
4264 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004265 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004266 return 0
4267
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004268 confirm_or_exit('This command will checkout all dependent branches and run '
4269 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004270
rmistry@google.com2dd99862015-06-22 12:22:18 +00004271 # Record all dependents that failed to upload.
4272 failures = {}
4273 # Go through all dependents, checkout the branch and upload.
4274 try:
4275 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004276 print()
4277 print('--------------------------------------')
4278 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004279 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004280 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004281 try:
4282 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004283 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004284 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004285 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004286 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004287 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004288 finally:
4289 # Swap back to the original root branch.
4290 RunGit(['checkout', '-q', root_branch])
4291
vapiera7fbd5a2016-06-16 09:17:49 -07004292 print()
4293 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004294 for dependent_branch in dependents:
4295 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004296 print(' %s : %s' % (dependent_branch, upload_status))
4297 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004298
4299 return 0
4300
4301
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004302@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004303def CMDarchive(parser, args):
4304 """Archives and deletes branches associated with closed changelists."""
4305 parser.add_option(
4306 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004307 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004308 parser.add_option(
4309 '-f', '--force', action='store_true',
4310 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004311 parser.add_option(
4312 '-d', '--dry-run', action='store_true',
4313 help='Skip the branch tagging and removal steps.')
4314 parser.add_option(
4315 '-t', '--notags', action='store_true',
4316 help='Do not tag archived branches. '
4317 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004318
4319 auth.add_auth_options(parser)
4320 options, args = parser.parse_args(args)
4321 if args:
4322 parser.error('Unsupported args: %s' % ' '.join(args))
4323 auth_config = auth.extract_auth_config_from_options(options)
4324
4325 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4326 if not branches:
4327 return 0
4328
vapiera7fbd5a2016-06-16 09:17:49 -07004329 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004330 changes = [Changelist(branchref=b, auth_config=auth_config)
4331 for b in branches.splitlines()]
4332 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4333 statuses = get_cl_statuses(changes,
4334 fine_grained=True,
4335 max_processes=options.maxjobs)
4336 proposal = [(cl.GetBranch(),
4337 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4338 for cl, status in statuses
4339 if status == 'closed']
4340 proposal.sort()
4341
4342 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004343 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004344 return 0
4345
4346 current_branch = GetCurrentBranch()
4347
vapiera7fbd5a2016-06-16 09:17:49 -07004348 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004349 if options.notags:
4350 for next_item in proposal:
4351 print(' ' + next_item[0])
4352 else:
4353 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4354 for next_item in proposal:
4355 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004356
kmarshall9249e012016-08-23 12:02:16 -07004357 # Quit now on precondition failure or if instructed by the user, either
4358 # via an interactive prompt or by command line flags.
4359 if options.dry_run:
4360 print('\nNo changes were made (dry run).\n')
4361 return 0
4362 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004363 print('You are currently on a branch \'%s\' which is associated with a '
4364 'closed codereview issue, so archive cannot proceed. Please '
4365 'checkout another branch and run this command again.' %
4366 current_branch)
4367 return 1
kmarshall9249e012016-08-23 12:02:16 -07004368 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004369 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4370 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004371 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004372 return 1
4373
4374 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004375 if not options.notags:
4376 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004377 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004378
vapiera7fbd5a2016-06-16 09:17:49 -07004379 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004380
4381 return 0
4382
4383
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004384@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004385def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004386 """Show status of changelists.
4387
4388 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004389 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004390 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004391 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004392 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004393 - Magenta in the commit queue
4394 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004395 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004396
4397 Also see 'git cl comments'.
4398 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004399 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004400 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004401 parser.add_option('-f', '--fast', action='store_true',
4402 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004403 parser.add_option(
4404 '-j', '--maxjobs', action='store', type=int,
4405 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004406
4407 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004408 _add_codereview_issue_select_options(
4409 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004410 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004411 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004412 if args:
4413 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004414 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004415
iannuccie53c9352016-08-17 14:40:40 -07004416 if options.issue is not None and not options.field:
4417 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004418
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004419 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004420 cl = Changelist(auth_config=auth_config, issue=options.issue,
4421 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004422 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004423 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004424 elif options.field == 'id':
4425 issueid = cl.GetIssue()
4426 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004427 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004428 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004429 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004430 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004431 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004432 elif options.field == 'status':
4433 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004434 elif options.field == 'url':
4435 url = cl.GetIssueURL()
4436 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004437 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004438 return 0
4439
4440 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4441 if not branches:
4442 print('No local branch found.')
4443 return 0
4444
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004445 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004446 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004447 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004448 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004449 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004450 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004451 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004452
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004453 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004454 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4455 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4456 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004457 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004458 c, status = output.next()
4459 branch_statuses[c.GetBranch()] = status
4460 status = branch_statuses.pop(branch)
4461 url = cl.GetIssueURL()
4462 if url and (not status or status == 'error'):
4463 # The issue probably doesn't exist anymore.
4464 url += ' (broken)'
4465
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004466 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004467 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004468 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004469 color = ''
4470 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004471 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004472 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004473 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004474 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004475
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004476
4477 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004478 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004479 print('Current branch: %s' % branch)
4480 for cl in changes:
4481 if cl.GetBranch() == branch:
4482 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004483 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004484 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004485 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004486 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004487 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004488 print('Issue description:')
4489 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004490 return 0
4491
4492
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004493def colorize_CMDstatus_doc():
4494 """To be called once in main() to add colors to git cl status help."""
4495 colors = [i for i in dir(Fore) if i[0].isupper()]
4496
4497 def colorize_line(line):
4498 for color in colors:
4499 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004500 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004501 indent = len(line) - len(line.lstrip(' ')) + 1
4502 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4503 return line
4504
4505 lines = CMDstatus.__doc__.splitlines()
4506 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4507
4508
phajdan.jre328cf92016-08-22 04:12:17 -07004509def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004510 if path == '-':
4511 json.dump(contents, sys.stdout)
4512 else:
4513 with open(path, 'w') as f:
4514 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004515
4516
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004517@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004518@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004519def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004520 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004521
4522 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004523 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004524 parser.add_option('-r', '--reverse', action='store_true',
4525 help='Lookup the branch(es) for the specified issues. If '
4526 'no issues are specified, all branches with mapped '
4527 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004528 parser.add_option('--json',
4529 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004530 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004531 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004532 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004533
dnj@chromium.org406c4402015-03-03 17:22:28 +00004534 if options.reverse:
4535 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004536 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004537 # Reverse issue lookup.
4538 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004539
4540 git_config = {}
4541 for config in RunGit(['config', '--get-regexp',
4542 r'branch\..*issue']).splitlines():
4543 name, _space, val = config.partition(' ')
4544 git_config[name] = val
4545
dnj@chromium.org406c4402015-03-03 17:22:28 +00004546 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004547 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4548 config_key = _git_branch_config_key(ShortBranchName(branch),
4549 cls.IssueConfigKey())
4550 issue = git_config.get(config_key)
4551 if issue:
4552 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004553 if not args:
4554 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004555 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004556 for issue in args:
4557 if not issue:
4558 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004559 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004560 print('Branch for issue number %s: %s' % (
4561 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004562 if options.json:
4563 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004564 return 0
4565
4566 if len(args) > 0:
4567 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4568 if not issue.valid:
4569 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4570 'or no argument to list it.\n'
4571 'Maybe you want to run git cl status?')
4572 cl = Changelist(codereview=issue.codereview)
4573 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004574 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004575 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004576 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4577 if options.json:
4578 write_json(options.json, {
4579 'issue': cl.GetIssue(),
4580 'issue_url': cl.GetIssueURL(),
4581 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004582 return 0
4583
4584
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004585@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004586def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004587 """Shows or posts review comments for any changelist."""
4588 parser.add_option('-a', '--add-comment', dest='comment',
4589 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004590 parser.add_option('-i', '--issue', dest='issue',
4591 help='review issue id (defaults to current issue). '
4592 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004593 parser.add_option('-m', '--machine-readable', dest='readable',
4594 action='store_false', default=True,
4595 help='output comments in a format compatible with '
4596 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004597 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004598 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004599 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004600 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004601 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004602 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004603 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004604
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004605 issue = None
4606 if options.issue:
4607 try:
4608 issue = int(options.issue)
4609 except ValueError:
4610 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004611 if not options.forced_codereview:
4612 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004613
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004614 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004615 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004616 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004617
4618 if options.comment:
4619 cl.AddComment(options.comment)
4620 return 0
4621
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004622 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4623 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004624 for comment in summary:
4625 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004626 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004627 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004628 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004629 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004630 color = Fore.MAGENTA
4631 else:
4632 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004633 print('\n%s%s %s%s\n%s' % (
4634 color,
4635 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4636 comment.sender,
4637 Fore.RESET,
4638 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4639
smut@google.comc85ac942015-09-15 16:34:43 +00004640 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004641 def pre_serialize(c):
4642 dct = c.__dict__.copy()
4643 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4644 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004645 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004646 return 0
4647
4648
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004649@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004650@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004651def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004652 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004653 parser.add_option('-d', '--display', action='store_true',
4654 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004655 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004656 help='New description to set for this issue (- for stdin, '
4657 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004658 parser.add_option('-f', '--force', action='store_true',
4659 help='Delete any unpublished Gerrit edits for this issue '
4660 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004661
4662 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004663 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004664 options, args = parser.parse_args(args)
4665 _process_codereview_select_options(parser, options)
4666
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004667 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004668 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004669 target_issue_arg = ParseIssueNumberArgument(args[0],
4670 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004671 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004672 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004673
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004674 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004675
martiniss6eda05f2016-06-30 10:18:35 -07004676 kwargs = {
4677 'auth_config': auth_config,
4678 'codereview': options.forced_codereview,
4679 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004680 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004681 if target_issue_arg:
4682 kwargs['issue'] = target_issue_arg.issue
4683 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004684 if target_issue_arg.codereview and not options.forced_codereview:
4685 detected_codereview_from_url = True
4686 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004687
4688 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004689 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004690 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004691 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004692
4693 if detected_codereview_from_url:
4694 logging.info('canonical issue/change URL: %s (type: %s)\n',
4695 cl.GetIssueURL(), target_issue_arg.codereview)
4696
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004697 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004698
smut@google.com34fb6b12015-07-13 20:03:26 +00004699 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004700 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004701 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004702
4703 if options.new_description:
4704 text = options.new_description
4705 if text == '-':
4706 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004707 elif text == '+':
4708 base_branch = cl.GetCommonAncestorWithUpstream()
4709 change = cl.GetChange(base_branch, None, local_description=True)
4710 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004711
4712 description.set_description(text)
4713 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004714 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004715
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004716 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004717 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004718 return 0
4719
4720
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004721def CreateDescriptionFromLog(args):
4722 """Pulls out the commit log to use as a base for the CL description."""
4723 log_args = []
4724 if len(args) == 1 and not args[0].endswith('.'):
4725 log_args = [args[0] + '..']
4726 elif len(args) == 1 and args[0].endswith('...'):
4727 log_args = [args[0][:-1]]
4728 elif len(args) == 2:
4729 log_args = [args[0] + '..' + args[1]]
4730 else:
4731 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004732 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004733
4734
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004735@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004736def CMDlint(parser, args):
4737 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004738 parser.add_option('--filter', action='append', metavar='-x,+y',
4739 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004740 auth.add_auth_options(parser)
4741 options, args = parser.parse_args(args)
4742 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004743
4744 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004745 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004746 try:
4747 import cpplint
4748 import cpplint_chromium
4749 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004750 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004751 return 1
4752
4753 # Change the current working directory before calling lint so that it
4754 # shows the correct base.
4755 previous_cwd = os.getcwd()
4756 os.chdir(settings.GetRoot())
4757 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004758 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004759 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4760 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004761 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004762 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004763 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004764
4765 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004766 command = args + files
4767 if options.filter:
4768 command = ['--filter=' + ','.join(options.filter)] + command
4769 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004770
4771 white_regex = re.compile(settings.GetLintRegex())
4772 black_regex = re.compile(settings.GetLintIgnoreRegex())
4773 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4774 for filename in filenames:
4775 if white_regex.match(filename):
4776 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004777 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004778 else:
4779 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4780 extra_check_functions)
4781 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004782 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004783 finally:
4784 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004785 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004786 if cpplint._cpplint_state.error_count != 0:
4787 return 1
4788 return 0
4789
4790
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004791@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004792def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004793 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004794 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004795 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004796 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004797 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004798 parser.add_option('--all', action='store_true',
4799 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004800 parser.add_option('--parallel', action='store_true',
4801 help='Run all tests specified by input_api.RunTests in all '
4802 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004803 auth.add_auth_options(parser)
4804 options, args = parser.parse_args(args)
4805 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004806
sbc@chromium.org71437c02015-04-09 19:29:40 +00004807 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004808 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004809 return 1
4810
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004811 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004812 if args:
4813 base_branch = args[0]
4814 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004815 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004816 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004817
Aaron Gable8076c282017-11-29 14:39:41 -08004818 if options.all:
4819 base_change = cl.GetChange(base_branch, None)
4820 files = [('M', f) for f in base_change.AllFiles()]
4821 change = presubmit_support.GitChange(
4822 base_change.Name(),
4823 base_change.FullDescriptionText(),
4824 base_change.RepositoryRoot(),
4825 files,
4826 base_change.issue,
4827 base_change.patchset,
4828 base_change.author_email,
4829 base_change._upstream)
4830 else:
4831 change = cl.GetChange(base_branch, None)
4832
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004833 cl.RunHook(
4834 committing=not options.upload,
4835 may_prompt=False,
4836 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004837 change=change,
4838 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004839 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004840
4841
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004842def GenerateGerritChangeId(message):
4843 """Returns Ixxxxxx...xxx change id.
4844
4845 Works the same way as
4846 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4847 but can be called on demand on all platforms.
4848
4849 The basic idea is to generate git hash of a state of the tree, original commit
4850 message, author/committer info and timestamps.
4851 """
4852 lines = []
4853 tree_hash = RunGitSilent(['write-tree'])
4854 lines.append('tree %s' % tree_hash.strip())
4855 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4856 if code == 0:
4857 lines.append('parent %s' % parent.strip())
4858 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4859 lines.append('author %s' % author.strip())
4860 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4861 lines.append('committer %s' % committer.strip())
4862 lines.append('')
4863 # Note: Gerrit's commit-hook actually cleans message of some lines and
4864 # whitespace. This code is not doing this, but it clearly won't decrease
4865 # entropy.
4866 lines.append(message)
4867 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4868 stdin='\n'.join(lines))
4869 return 'I%s' % change_hash.strip()
4870
4871
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004872def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004873 """Computes the remote branch ref to use for the CL.
4874
4875 Args:
4876 remote (str): The git remote for the CL.
4877 remote_branch (str): The git remote branch for the CL.
4878 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004879 """
4880 if not (remote and remote_branch):
4881 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004882
wittman@chromium.org455dc922015-01-26 20:15:50 +00004883 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004884 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004885 # refs, which are then translated into the remote full symbolic refs
4886 # below.
4887 if '/' not in target_branch:
4888 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4889 else:
4890 prefix_replacements = (
4891 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4892 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4893 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4894 )
4895 match = None
4896 for regex, replacement in prefix_replacements:
4897 match = re.search(regex, target_branch)
4898 if match:
4899 remote_branch = target_branch.replace(match.group(0), replacement)
4900 break
4901 if not match:
4902 # This is a branch path but not one we recognize; use as-is.
4903 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004904 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4905 # Handle the refs that need to land in different refs.
4906 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004907
wittman@chromium.org455dc922015-01-26 20:15:50 +00004908 # Create the true path to the remote branch.
4909 # Does the following translation:
4910 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4911 # * refs/remotes/origin/master -> refs/heads/master
4912 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4913 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4914 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4915 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4916 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4917 'refs/heads/')
4918 elif remote_branch.startswith('refs/remotes/branch-heads'):
4919 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004920
wittman@chromium.org455dc922015-01-26 20:15:50 +00004921 return remote_branch
4922
4923
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004924def cleanup_list(l):
4925 """Fixes a list so that comma separated items are put as individual items.
4926
4927 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4928 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4929 """
4930 items = sum((i.split(',') for i in l), [])
4931 stripped_items = (i.strip() for i in items)
4932 return sorted(filter(None, stripped_items))
4933
4934
Aaron Gable4db38df2017-11-03 14:59:07 -07004935@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004936@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004937def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004938 """Uploads the current changelist to codereview.
4939
4940 Can skip dependency patchset uploads for a branch by running:
4941 git config branch.branch_name.skip-deps-uploads True
4942 To unset run:
4943 git config --unset branch.branch_name.skip-deps-uploads
4944 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004945
4946 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4947 a bug number, this bug number is automatically populated in the CL
4948 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004949
4950 If subject contains text in square brackets or has "<text>: " prefix, such
4951 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4952 [git-cl] add support for hashtags
4953 Foo bar: implement foo
4954 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004955 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004956 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4957 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004958 parser.add_option('--bypass-watchlists', action='store_true',
4959 dest='bypass_watchlists',
4960 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004961 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004962 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004963 parser.add_option('--message', '-m', dest='message',
4964 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004965 parser.add_option('-b', '--bug',
4966 help='pre-populate the bug number(s) for this issue. '
4967 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004968 parser.add_option('--message-file', dest='message_file',
4969 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004970 parser.add_option('--title', '-t', dest='title',
4971 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004972 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004973 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004974 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004975 parser.add_option('--tbrs',
4976 action='append', default=[],
4977 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004978 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004979 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004980 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004981 parser.add_option('--hashtag', dest='hashtags',
4982 action='append', default=[],
4983 help=('Gerrit hashtag for new CL; '
4984 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004985 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004986 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004987 parser.add_option('--emulate_svn_auto_props',
4988 '--emulate-svn-auto-props',
4989 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004990 dest="emulate_svn_auto_props",
4991 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004992 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004993 help='tell the commit queue to commit this patchset; '
4994 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004995 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004996 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004997 metavar='TARGET',
4998 help='Apply CL to remote ref TARGET. ' +
4999 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005000 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005001 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00005002 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005003 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005004 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005005 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005006 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5007 const='TBR', help='add a set of OWNERS to TBR')
5008 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5009 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005010 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5011 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005012 help='Send the patchset to do a CQ dry run right after '
5013 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005014 parser.add_option('--dependencies', action='store_true',
5015 help='Uploads CLs of all the local branches that depend on '
5016 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04005017 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5018 help='Sends your change to the CQ after an approval. Only '
5019 'works on repos that have the Auto-Submit label '
5020 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04005021 parser.add_option('--parallel', action='store_true',
5022 help='Run all tests specified by input_api.RunTests in all '
5023 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005024
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005025 parser.add_option('--no-autocc', action='store_true',
5026 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005027 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005028 help='Set the review private. This implies --no-autocc.')
5029
5030 # TODO: remove Rietveld flags
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005031 parser.add_option('--email', default=None,
5032 help='email address to use to connect to Rietveld')
5033
rmistry@google.com2dd99862015-06-22 12:22:18 +00005034 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005035 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005036 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005037 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005038 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005039 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005040
sbc@chromium.org71437c02015-04-09 19:29:40 +00005041 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005042 return 1
5043
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005044 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005045 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005046 options.cc = cleanup_list(options.cc)
5047
tandriib80458a2016-06-23 12:20:07 -07005048 if options.message_file:
5049 if options.message:
5050 parser.error('only one of --message and --message-file allowed.')
5051 options.message = gclient_utils.FileRead(options.message_file)
5052 options.message_file = None
5053
tandrii4d0545a2016-07-06 03:56:49 -07005054 if options.cq_dry_run and options.use_commit_queue:
5055 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5056
Aaron Gableedbc4132017-09-11 13:22:28 -07005057 if options.use_commit_queue:
5058 options.send_mail = True
5059
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005060 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5061 settings.GetIsGerrit()
5062
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005063 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00005064 if not cl.IsGerrit():
5065 # Error out with instructions for repos not yet configured for Gerrit.
5066 print('=====================================')
5067 print('NOTICE: Rietveld is no longer supported. '
5068 'You can upload changes to Gerrit with')
5069 print(' git cl upload --gerrit')
5070 print('or set Gerrit to be your default code review tool with')
5071 print(' git config gerrit.host true')
5072 print('=====================================')
5073 return 1
5074
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005075 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005076
5077
Francois Dorayd42c6812017-05-30 15:10:20 -04005078@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005079@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005080def CMDsplit(parser, args):
5081 """Splits a branch into smaller branches and uploads CLs.
5082
5083 Creates a branch and uploads a CL for each group of files modified in the
5084 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005085 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005086 the shared OWNERS file.
5087 """
5088 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005089 help="A text file containing a CL description in which "
5090 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005091 parser.add_option("-c", "--comment", dest="comment_file",
5092 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005093 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5094 default=False,
5095 help="List the files and reviewers for each CL that would "
5096 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00005097 parser.add_option("--cq-dry-run", action='store_true',
5098 help="If set, will do a cq dry run for each uploaded CL. "
5099 "Please be careful when doing this; more than ~10 CLs "
5100 "has the potential to overload our build "
5101 "infrastructure. Try to upload these not during high "
5102 "load times (usually 11-3 Mountain View time). Email "
5103 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005104 options, _ = parser.parse_args(args)
5105
5106 if not options.description_file:
5107 parser.error('No --description flag specified.')
5108
5109 def WrappedCMDupload(args):
5110 return CMDupload(OptionParser(), args)
5111
5112 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00005113 Changelist, WrappedCMDupload, options.dry_run,
5114 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005115
5116
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005117@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005118@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005119def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005120 """DEPRECATED: Used to commit the current changelist via git-svn."""
5121 message = ('git-cl no longer supports committing to SVN repositories via '
5122 'git-svn. You probably want to use `git cl land` instead.')
5123 print(message)
5124 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005125
5126
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005127# Two special branches used by git cl land.
5128MERGE_BRANCH = 'git-cl-commit'
5129CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5130
5131
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005132@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005133@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005134def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005135 """Commits the current changelist via git.
5136
5137 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5138 upstream and closes the issue automatically and atomically.
5139
5140 Otherwise (in case of Rietveld):
5141 Squashes branch into a single commit.
5142 Updates commit message with metadata (e.g. pointer to review).
5143 Pushes the code upstream.
5144 Updates review and closes.
5145 """
5146 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5147 help='bypass upload presubmit hook')
5148 parser.add_option('-m', dest='message',
5149 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005150 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005151 help="force yes to questions (don't prompt)")
5152 parser.add_option('-c', dest='contributor',
5153 help="external contributor for patch (appended to " +
5154 "description and used as author for git). Should be " +
5155 "formatted as 'First Last <email@example.com>'")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005156 parser.add_option('--parallel', action='store_true',
5157 help='Run all tests specified by input_api.RunTests in all '
5158 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005159 auth.add_auth_options(parser)
5160 (options, args) = parser.parse_args(args)
5161 auth_config = auth.extract_auth_config_from_options(options)
5162
5163 cl = Changelist(auth_config=auth_config)
5164
Robert Iannucci2e73d432018-03-14 01:10:47 -07005165 if not cl.IsGerrit():
5166 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005167
Robert Iannucci2e73d432018-03-14 01:10:47 -07005168 if options.message:
5169 # This could be implemented, but it requires sending a new patch to
5170 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5171 # Besides, Gerrit has the ability to change the commit message on submit
5172 # automatically, thus there is no need to support this option (so far?).
5173 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005174 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005175 parser.error(
5176 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5177 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5178 'the contributor\'s "name <email>". If you can\'t upload such a '
5179 'commit for review, contact your repository admin and request'
5180 '"Forge-Author" permission.')
5181 if not cl.GetIssue():
5182 DieWithError('You must upload the change first to Gerrit.\n'
5183 ' If you would rather have `git cl land` upload '
5184 'automatically for you, see http://crbug.com/642759')
5185 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005186 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005187
5188
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005189def PushToGitWithAutoRebase(remote, branch, original_description,
5190 git_numberer_enabled, max_attempts=3):
5191 """Pushes current HEAD commit on top of remote's branch.
5192
5193 Attempts to fetch and autorebase on push failures.
5194 Adds git number footers on the fly.
5195
5196 Returns integer code from last command.
5197 """
5198 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5199 code = 0
5200 attempts_left = max_attempts
5201 while attempts_left:
5202 attempts_left -= 1
5203 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5204
5205 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5206 # If fetch fails, retry.
5207 print('Fetching %s/%s...' % (remote, branch))
5208 code, out = RunGitWithCode(
5209 ['retry', 'fetch', remote,
5210 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5211 if code:
5212 print('Fetch failed with exit code %d.' % code)
5213 print(out.strip())
5214 continue
5215
5216 print('Cherry-picking commit on top of latest %s' % branch)
5217 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5218 suppress_stderr=True)
5219 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5220 code, out = RunGitWithCode(['cherry-pick', cherry])
5221 if code:
5222 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5223 'the following files have merge conflicts:' %
5224 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005225 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5226 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005227 print('Please rebase your patch and try again.')
5228 RunGitWithCode(['cherry-pick', '--abort'])
5229 break
5230
5231 commit_desc = ChangeDescription(original_description)
5232 if git_numberer_enabled:
5233 logging.debug('Adding git number footers')
5234 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5235 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5236 branch)
5237 # Ensure timestamps are monotonically increasing.
5238 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5239 _get_committer_timestamp('HEAD'))
5240 _git_amend_head(commit_desc.description, timestamp)
5241
5242 code, out = RunGitWithCode(
5243 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5244 print(out)
5245 if code == 0:
5246 break
5247 if IsFatalPushFailure(out):
5248 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005249 'user.email are correct and you have push access to the repo.\n'
5250 'Hint: run command below to diangose common Git/Gerrit credential '
5251 'problems:\n'
5252 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005253 break
5254 return code
5255
5256
5257def IsFatalPushFailure(push_stdout):
5258 """True if retrying push won't help."""
5259 return '(prohibited by Gerrit)' in push_stdout
5260
5261
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005262@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005263@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005264def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005265 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005266 parser.add_option('-b', dest='newbranch',
5267 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005268 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005269 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005270 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005271 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005272 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005273 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005274 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005275 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005276 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005277 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005278
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005279
5280 group = optparse.OptionGroup(
5281 parser,
5282 'Options for continuing work on the current issue uploaded from a '
5283 'different clone (e.g. different machine). Must be used independently '
5284 'from the other options. No issue number should be specified, and the '
5285 'branch must have an issue number associated with it')
5286 group.add_option('--reapply', action='store_true', dest='reapply',
5287 help='Reset the branch and reapply the issue.\n'
5288 'CAUTION: This will undo any local changes in this '
5289 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005290
5291 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005292 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005293 parser.add_option_group(group)
5294
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005295 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005296 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005297 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005298 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005299 auth_config = auth.extract_auth_config_from_options(options)
5300
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005301 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005302 if options.newbranch:
5303 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005304 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005305 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005306
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005307 cl = Changelist(auth_config=auth_config,
5308 codereview=options.forced_codereview)
5309 if not cl.GetIssue():
5310 parser.error('current branch must have an associated issue')
5311
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005312 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005313 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005314 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005315
5316 RunGit(['reset', '--hard', upstream])
5317 if options.pull:
5318 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005319
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005320 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5321 options.directory)
5322
5323 if len(args) != 1 or not args[0]:
5324 parser.error('Must specify issue number or url')
5325
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005326 target_issue_arg = ParseIssueNumberArgument(args[0],
5327 options.forced_codereview)
5328 if not target_issue_arg.valid:
5329 parser.error('invalid codereview url or CL id')
5330
5331 cl_kwargs = {
5332 'auth_config': auth_config,
5333 'codereview_host': target_issue_arg.hostname,
5334 'codereview': options.forced_codereview,
5335 }
5336 detected_codereview_from_url = False
5337 if target_issue_arg.codereview and not options.forced_codereview:
5338 detected_codereview_from_url = True
5339 cl_kwargs['codereview'] = target_issue_arg.codereview
5340 cl_kwargs['issue'] = target_issue_arg.issue
5341
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005342 # We don't want uncommitted changes mixed up with the patch.
5343 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005344 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005345
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005346 if options.newbranch:
5347 if options.force:
5348 RunGit(['branch', '-D', options.newbranch],
5349 stderr=subprocess2.PIPE, error_ok=True)
5350 RunGit(['new-branch', options.newbranch])
5351
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005352 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005353
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005354 if cl.IsGerrit():
5355 if options.reject:
5356 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005357 if options.directory:
5358 parser.error('--directory is not supported with Gerrit codereview.')
5359
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005360 if detected_codereview_from_url:
5361 print('canonical issue/change URL: %s (type: %s)\n' %
5362 (cl.GetIssueURL(), target_issue_arg.codereview))
5363
5364 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005365 options.nocommit, options.directory,
5366 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005367
5368
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005369def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005370 """Fetches the tree status and returns either 'open', 'closed',
5371 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005372 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005373 if url:
5374 status = urllib2.urlopen(url).read().lower()
5375 if status.find('closed') != -1 or status == '0':
5376 return 'closed'
5377 elif status.find('open') != -1 or status == '1':
5378 return 'open'
5379 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005380 return 'unset'
5381
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005382
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005383def GetTreeStatusReason():
5384 """Fetches the tree status from a json url and returns the message
5385 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005386 url = settings.GetTreeStatusUrl()
5387 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005388 connection = urllib2.urlopen(json_url)
5389 status = json.loads(connection.read())
5390 connection.close()
5391 return status['message']
5392
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005393
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005394@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005395def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005396 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005397 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005398 status = GetTreeStatus()
5399 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005400 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005401 return 2
5402
vapiera7fbd5a2016-06-16 09:17:49 -07005403 print('The tree is %s' % status)
5404 print()
5405 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005406 if status != 'open':
5407 return 1
5408 return 0
5409
5410
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005411@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005412def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005413 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005414 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005415 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005416 '-b', '--bot', action='append',
5417 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5418 'times to specify multiple builders. ex: '
5419 '"-b win_rel -b win_layout". See '
5420 'the try server waterfall for the builders name and the tests '
5421 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005422 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005423 '-B', '--bucket', default='',
5424 help=('Buildbucket bucket to send the try requests.'))
5425 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005426 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005427 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005428 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005429 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005430 help='Revision to use for the try job; default: the revision will '
5431 'be determined by the try recipe that builder runs, which usually '
5432 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005433 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005434 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005435 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005436 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005437 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005438 '--category', default='git_cl_try', help='Specify custom build category.')
5439 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005440 '--project',
5441 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005442 'in recipe to determine to which repository or directory to '
5443 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005444 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005445 '-p', '--property', dest='properties', action='append', default=[],
5446 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005447 'key2=value2 etc. The value will be treated as '
5448 'json if decodable, or as string otherwise. '
5449 'NOTE: using this may make your try job not usable for CQ, '
5450 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005451 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005452 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5453 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005454 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005455 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005456 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005457 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005458 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005459 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005460
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005461 if options.master and options.master.startswith('luci.'):
5462 parser.error(
5463 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005464 # Make sure that all properties are prop=value pairs.
5465 bad_params = [x for x in options.properties if '=' not in x]
5466 if bad_params:
5467 parser.error('Got properties with missing "=": %s' % bad_params)
5468
maruel@chromium.org15192402012-09-06 12:38:29 +00005469 if args:
5470 parser.error('Unknown arguments: %s' % args)
5471
Koji Ishii31c14782018-01-08 17:17:33 +09005472 cl = Changelist(auth_config=auth_config, issue=options.issue,
5473 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005474 if not cl.GetIssue():
5475 parser.error('Need to upload first')
5476
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005477 if cl.IsGerrit():
5478 # HACK: warm up Gerrit change detail cache to save on RPCs.
5479 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5480
tandriie113dfd2016-10-11 10:20:12 -07005481 error_message = cl.CannotTriggerTryJobReason()
5482 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005483 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005484
borenet6c0efe62016-10-19 08:13:29 -07005485 if options.bucket and options.master:
5486 parser.error('Only one of --bucket and --master may be used.')
5487
qyearsley1fdfcb62016-10-24 13:22:03 -07005488 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005489
qyearsleydd49f942016-10-28 11:57:22 -07005490 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5491 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005492 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005493 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005494 print('git cl try with no bots now defaults to CQ dry run.')
5495 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5496 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005497
borenet6c0efe62016-10-19 08:13:29 -07005498 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005499 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005500 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005501 'of bot requires an initial job from a parent (usually a builder). '
5502 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005503 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005504 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005505
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005506 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005507 # TODO(tandrii): Checking local patchset against remote patchset is only
5508 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5509 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005510 print('Warning: Codereview server has newer patchsets (%s) than most '
5511 'recent upload from local checkout (%s). Did a previous upload '
5512 'fail?\n'
5513 'By default, git cl try uses the latest patchset from '
5514 'codereview, continuing to use patchset %s.\n' %
5515 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005516
tandrii568043b2016-10-11 07:49:18 -07005517 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005518 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005519 except BuildbucketResponseException as ex:
5520 print('ERROR: %s' % ex)
5521 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005522 return 0
5523
5524
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005525@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005526def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005527 """Prints info about try jobs associated with current CL."""
5528 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005529 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005530 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005531 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005532 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005533 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005534 '--color', action='store_true', default=setup_color.IS_TTY,
5535 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005536 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005537 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5538 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005539 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005540 '--json', help=('Path of JSON output file to write try job results to,'
5541 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005542 parser.add_option_group(group)
5543 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005544 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005545 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005546 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005547 if args:
5548 parser.error('Unrecognized args: %s' % ' '.join(args))
5549
5550 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005551 cl = Changelist(
5552 issue=options.issue, codereview=options.forced_codereview,
5553 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005554 if not cl.GetIssue():
5555 parser.error('Need to upload first')
5556
tandrii221ab252016-10-06 08:12:04 -07005557 patchset = options.patchset
5558 if not patchset:
5559 patchset = cl.GetMostRecentPatchset()
5560 if not patchset:
5561 parser.error('Codereview doesn\'t know about issue %s. '
5562 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005563 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005564 cl.GetIssue())
5565
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005566 # TODO(tandrii): Checking local patchset against remote patchset is only
5567 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5568 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005569 print('Warning: Codereview server has newer patchsets (%s) than most '
5570 'recent upload from local checkout (%s). Did a previous upload '
5571 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005572 'By default, git cl try-results uses the latest patchset from '
5573 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005574 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005575 try:
tandrii221ab252016-10-06 08:12:04 -07005576 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005577 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005578 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005579 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005580 if options.json:
5581 write_try_results_json(options.json, jobs)
5582 else:
5583 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005584 return 0
5585
5586
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005587@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005588@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005589def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005590 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005591 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005592 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005593 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005594
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005595 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005596 if args:
5597 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005598 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005599 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005600 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005601 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005602
5603 # Clear configured merge-base, if there is one.
5604 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005605 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005606 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005607 return 0
5608
5609
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005610@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005611def CMDweb(parser, args):
5612 """Opens the current CL in the web browser."""
5613 _, args = parser.parse_args(args)
5614 if args:
5615 parser.error('Unrecognized args: %s' % ' '.join(args))
5616
5617 issue_url = Changelist().GetIssueURL()
5618 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005619 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005620 return 1
5621
5622 webbrowser.open(issue_url)
5623 return 0
5624
5625
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005626@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005627def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005628 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005629 parser.add_option('-d', '--dry-run', action='store_true',
5630 help='trigger in dry run mode')
5631 parser.add_option('-c', '--clear', action='store_true',
5632 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005633 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005634 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005635 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005636 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005637 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005638 if args:
5639 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005640 if options.dry_run and options.clear:
5641 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5642
iannuccie53c9352016-08-17 14:40:40 -07005643 cl = Changelist(auth_config=auth_config, issue=options.issue,
5644 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005645 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005646 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005647 elif options.dry_run:
5648 state = _CQState.DRY_RUN
5649 else:
5650 state = _CQState.COMMIT
5651 if not cl.GetIssue():
5652 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005653 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005654 return 0
5655
5656
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005657@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005658def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005659 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005660 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005661 auth.add_auth_options(parser)
5662 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005663 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005664 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005665 if args:
5666 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005667 cl = Changelist(auth_config=auth_config, issue=options.issue,
5668 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005669 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005670 if not cl.GetIssue():
5671 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005672 cl.CloseIssue()
5673 return 0
5674
5675
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005676@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005677def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005678 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005679 parser.add_option(
5680 '--stat',
5681 action='store_true',
5682 dest='stat',
5683 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005684 auth.add_auth_options(parser)
5685 options, args = parser.parse_args(args)
5686 auth_config = auth.extract_auth_config_from_options(options)
5687 if args:
5688 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005689
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005690 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005691 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005692 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005693 if not issue:
5694 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005695
Aaron Gablea718c3e2017-08-28 17:47:28 -07005696 base = cl._GitGetBranchConfigValue('last-upload-hash')
5697 if not base:
5698 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5699 if not base:
5700 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5701 revision_info = detail['revisions'][detail['current_revision']]
5702 fetch_info = revision_info['fetch']['http']
5703 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5704 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005705
Aaron Gablea718c3e2017-08-28 17:47:28 -07005706 cmd = ['git', 'diff']
5707 if options.stat:
5708 cmd.append('--stat')
5709 cmd.append(base)
5710 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005711
5712 return 0
5713
5714
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005715@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005716def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005717 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005718 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005719 '--ignore-current',
5720 action='store_true',
5721 help='Ignore the CL\'s current reviewers and start from scratch.')
5722 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005723 '--no-color',
5724 action='store_true',
5725 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005726 parser.add_option(
5727 '--batch',
5728 action='store_true',
5729 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005730 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005731 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005732 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005733
5734 author = RunGit(['config', 'user.email']).strip() or None
5735
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005736 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005737
5738 if args:
5739 if len(args) > 1:
5740 parser.error('Unknown args')
5741 base_branch = args[0]
5742 else:
5743 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005744 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005745
5746 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005747 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5748
5749 if options.batch:
5750 db = owners.Database(change.RepositoryRoot(), file, os.path)
5751 print('\n'.join(db.reviewers_for(affected_files, author)))
5752 return 0
5753
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005754 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005755 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005756 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005757 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005758 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005759 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005760 disable_color=options.no_color,
5761 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005762
5763
Aiden Bennerc08566e2018-10-03 17:52:42 +00005764def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005765 """Generates a diff command."""
5766 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005767 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5768
5769 if not allow_prefix:
5770 diff_cmd += ['--no-prefix']
5771
5772 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005773
5774 if args:
5775 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005776 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005777 diff_cmd.append(arg)
5778 else:
5779 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005780
5781 return diff_cmd
5782
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005783
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005784def MatchingFileType(file_name, extensions):
5785 """Returns true if the file name ends with one of the given extensions."""
5786 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005787
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005788
enne@chromium.org555cfe42014-01-29 18:21:39 +00005789@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005790@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005791def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005792 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005793 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005794 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005795 parser.add_option('--full', action='store_true',
5796 help='Reformat the full content of all touched files')
5797 parser.add_option('--dry-run', action='store_true',
5798 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005799 parser.add_option('--python', action='store_true',
5800 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005801 parser.add_option('--js', action='store_true',
5802 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005803 parser.add_option('--diff', action='store_true',
5804 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005805 parser.add_option('--presubmit', action='store_true',
5806 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005807 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005808
Daniel Chengc55eecf2016-12-30 03:11:02 -08005809 # Normalize any remaining args against the current path, so paths relative to
5810 # the current directory are still resolved as expected.
5811 args = [os.path.join(os.getcwd(), arg) for arg in args]
5812
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005813 # git diff generates paths against the root of the repository. Change
5814 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005815 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005816 if rel_base_path:
5817 os.chdir(rel_base_path)
5818
digit@chromium.org29e47272013-05-17 17:01:46 +00005819 # Grab the merge-base commit, i.e. the upstream commit of the current
5820 # branch when it was created or the last time it was rebased. This is
5821 # to cover the case where the user may have called "git fetch origin",
5822 # moving the origin branch to a newer commit, but hasn't rebased yet.
5823 upstream_commit = None
5824 cl = Changelist()
5825 upstream_branch = cl.GetUpstreamBranch()
5826 if upstream_branch:
5827 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5828 upstream_commit = upstream_commit.strip()
5829
5830 if not upstream_commit:
5831 DieWithError('Could not find base commit for this branch. '
5832 'Are you in detached state?')
5833
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005834 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5835 diff_output = RunGit(changed_files_cmd)
5836 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005837 # Filter out files deleted by this CL
5838 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005839
Christopher Lamc5ba6922017-01-24 11:19:14 +11005840 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005841 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005842
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005843 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5844 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5845 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005846 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005847
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005848 top_dir = os.path.normpath(
5849 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5850
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005851 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5852 # formatted. This is used to block during the presubmit.
5853 return_value = 0
5854
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005855 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005856 # Locate the clang-format binary in the checkout
5857 try:
5858 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005859 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005860 DieWithError(e)
5861
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005862 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005863 cmd = [clang_format_tool]
5864 if not opts.dry_run and not opts.diff:
5865 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005866 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005867 if opts.diff:
5868 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005869 else:
5870 env = os.environ.copy()
5871 env['PATH'] = str(os.path.dirname(clang_format_tool))
5872 try:
5873 script = clang_format.FindClangFormatScriptInChromiumTree(
5874 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005875 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005876 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005877
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005878 cmd = [sys.executable, script, '-p0']
5879 if not opts.dry_run and not opts.diff:
5880 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005881
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005882 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5883 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005884
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005885 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5886 if opts.diff:
5887 sys.stdout.write(stdout)
5888 if opts.dry_run and len(stdout) > 0:
5889 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005890
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005891 # Similar code to above, but using yapf on .py files rather than clang-format
5892 # on C/C++ files
Aiden Bennerc08566e2018-10-03 17:52:42 +00005893 if opts.python and python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005894 yapf_tool = gclient_utils.FindExecutable('yapf')
5895 if yapf_tool is None:
5896 DieWithError('yapf not found in PATH')
5897
Aiden Bennerc08566e2018-10-03 17:52:42 +00005898 # If we couldn't find a yapf file we'll default to the chromium style
5899 # specified in depot_tools.
5900 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5901 chromium_default_yapf_style = os.path.join(depot_tools_path,
5902 YAPF_CONFIG_FILENAME)
5903
5904 # Note: yapf still seems to fix indentation of the entire file
5905 # even if line ranges are specified.
5906 # See https://github.com/google/yapf/issues/499
5907 if not opts.full:
5908 py_line_diffs = _ComputeDiffLineRanges(python_diff_files, upstream_commit)
5909
5910 # Used for caching.
5911 yapf_configs = {}
5912 for f in python_diff_files:
5913 # Find the yapf style config for the current file, defaults to depot
5914 # tools default.
5915 yapf_config = _FindYapfConfigFile(
5916 os.path.abspath(f), yapf_configs, top_dir,
5917 chromium_default_yapf_style)
5918
5919 cmd = [yapf_tool, '--style', yapf_config, f]
5920
5921 has_formattable_lines = False
5922 if not opts.full:
5923 # Only run yapf over changed line ranges.
5924 for diff_start, diff_len in py_line_diffs[f]:
5925 diff_end = diff_start + diff_len - 1
5926 # Yapf errors out if diff_end < diff_start but this
5927 # is a valid line range diff for a removal.
5928 if diff_end >= diff_start:
5929 has_formattable_lines = True
5930 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5931 # If all line diffs were removals we have nothing to format.
5932 if not has_formattable_lines:
5933 continue
5934
5935 if opts.diff or opts.dry_run:
5936 cmd += ['--diff']
5937 # Will return non-zero exit code if non-empty diff.
5938 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5939 if opts.diff:
5940 sys.stdout.write(stdout)
5941 elif len(stdout) > 0:
5942 return_value = 2
5943 else:
5944 cmd += ['-i']
5945 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005946
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005947 # Dart's formatter does not have the nice property of only operating on
5948 # modified chunks, so hard code full.
5949 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005950 try:
5951 command = [dart_format.FindDartFmtToolInChromiumTree()]
5952 if not opts.dry_run and not opts.diff:
5953 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005954 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005955
ppi@chromium.org6593d932016-03-03 15:41:15 +00005956 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005957 if opts.dry_run and stdout:
5958 return_value = 2
5959 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005960 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5961 'found in this checkout. Files in other languages are still '
5962 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005963
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005964 # Format GN build files. Always run on full build files for canonical form.
5965 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005966 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005967 if opts.dry_run or opts.diff:
5968 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005969 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005970 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5971 shell=sys.platform == 'win32',
5972 cwd=top_dir)
5973 if opts.dry_run and gn_ret == 2:
5974 return_value = 2 # Not formatted.
5975 elif opts.diff and gn_ret == 2:
5976 # TODO this should compute and print the actual diff.
5977 print("This change has GN build file diff for " + gn_diff_file)
5978 elif gn_ret != 0:
5979 # For non-dry run cases (and non-2 return values for dry-run), a
5980 # nonzero error code indicates a failure, probably because the file
5981 # doesn't parse.
5982 DieWithError("gn format failed on " + gn_diff_file +
5983 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005984
Ilya Shermane081cbe2017-08-15 17:51:04 -07005985 # Skip the metrics formatting from the global presubmit hook. These files have
5986 # a separate presubmit hook that issues an error if the files need formatting,
5987 # whereas the top-level presubmit script merely issues a warning. Formatting
5988 # these files is somewhat slow, so it's important not to duplicate the work.
5989 if not opts.presubmit:
5990 for xml_dir in GetDirtyMetricsDirs(diff_files):
5991 tool_dir = os.path.join(top_dir, xml_dir)
5992 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5993 if opts.dry_run or opts.diff:
5994 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005995 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005996 if opts.diff:
5997 sys.stdout.write(stdout)
5998 if opts.dry_run and stdout:
5999 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006000
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006001 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006002
Steven Holte2e664bf2017-04-21 13:10:47 -07006003def GetDirtyMetricsDirs(diff_files):
6004 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
6005 metrics_xml_dirs = [
6006 os.path.join('tools', 'metrics', 'actions'),
6007 os.path.join('tools', 'metrics', 'histograms'),
6008 os.path.join('tools', 'metrics', 'rappor'),
6009 os.path.join('tools', 'metrics', 'ukm')]
6010 for xml_dir in metrics_xml_dirs:
6011 if any(file.startswith(xml_dir) for file in xml_diff_files):
6012 yield xml_dir
6013
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006014
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006015@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006016@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006017def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006018 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006019 _, args = parser.parse_args(args)
6020
6021 if len(args) != 1:
6022 parser.print_help()
6023 return 1
6024
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006025 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006026 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006027 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006028
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006029 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006030
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006031 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006032 output = RunGit(['config', '--local', '--get-regexp',
6033 r'branch\..*\.%s' % issueprefix],
6034 error_ok=True)
6035 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006036 if issue == target_issue:
6037 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006038
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006039 branches = []
6040 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006041 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006042 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006043 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006044 return 1
6045 if len(branches) == 1:
6046 RunGit(['checkout', branches[0]])
6047 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006048 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006049 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006050 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006051 which = raw_input('Choose by index: ')
6052 try:
6053 RunGit(['checkout', branches[int(which)]])
6054 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006055 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006056 return 1
6057
6058 return 0
6059
6060
maruel@chromium.org29404b52014-09-08 22:58:00 +00006061def CMDlol(parser, args):
6062 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006063 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006064 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6065 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6066 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006067 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006068 return 0
6069
6070
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006071class OptionParser(optparse.OptionParser):
6072 """Creates the option parse and add --verbose support."""
6073 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006074 optparse.OptionParser.__init__(
6075 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006076 self.add_option(
6077 '-v', '--verbose', action='count', default=0,
6078 help='Use 2 times for more debugging info')
6079
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006080 def parse_args(self, args=None, _values=None):
6081 # Create an optparse.Values object that will store only the actual passed
6082 # options, without the defaults.
6083 actual_options = optparse.Values()
6084 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6085 # Create an optparse.Values object with the default options.
6086 options = optparse.Values(self.get_default_values().__dict__)
6087 # Update it with the options passed by the user.
6088 options._update_careful(actual_options.__dict__)
6089 # Store the options passed by the user in an _actual_options attribute.
6090 # We store only the keys, and not the values, since the values can contain
6091 # arbitrary information, which might be PII.
6092 metrics.collector.add('arguments', actual_options.__dict__.keys())
6093
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006094 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006095 logging.basicConfig(
6096 level=levels[min(options.verbose, len(levels) - 1)],
6097 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6098 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00006099
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006100 return options, args
6101
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006102
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006103def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006104 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006105 print('\nYour python version %s is unsupported, please upgrade.\n' %
6106 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006107 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006108
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006109 # Reload settings.
6110 global settings
6111 settings = Settings()
6112
Edward Lemurad463c92018-07-25 21:31:23 +00006113 if not metrics.DISABLE_METRICS_COLLECTION:
6114 metrics.collector.add('project_urls', [settings.GetViewVCUrl().strip('/+')])
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006115 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006116 dispatcher = subcommand.CommandDispatcher(__name__)
6117 try:
6118 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006119 except auth.AuthenticationError as e:
6120 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006121 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006122 if e.code != 500:
6123 raise
6124 DieWithError(
6125 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6126 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006127 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006128
6129
6130if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006131 # These affect sys.stdout so do it outside of main() to simplify mocks in
6132 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006133 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006134 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00006135 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00006136 sys.exit(main(sys.argv[1:]))