blob: 2d3b54c72216fd34b5e068d1df976def3ae9082e [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
Edward Lemurfec80c42018-11-01 23:14:14 +000032import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000033import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000035import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000036import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000037import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000038import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000039
40try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080041 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042except ImportError:
43 pass
44
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000045from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000046from third_party import httplib2
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
Edward Lesmes93277a72018-10-18 22:04:26 +000059import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000060import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000061import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import presubmit_support
63import 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'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080072POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000074REFS_THAT_ALIAS_TO_OTHER_REFS = {
75 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
76 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
77}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078
thestig@chromium.org44202a22014-03-11 19:22:18 +000079# Valid extensions for files we want to lint.
80DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
81DEFAULT_LINT_IGNORE_REGEX = r"$^"
82
Aiden Bennerc08566e2018-10-03 17:52:42 +000083# File name for yapf style config files.
84YAPF_CONFIG_FILENAME = '.style.yapf'
85
borenet6c0efe62016-10-19 08:13:29 -070086# Buildbucket master name prefix.
87MASTER_PREFIX = 'master.'
88
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000089# Shortcut since it quickly becomes redundant.
90Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000091
maruel@chromium.orgddd59412011-11-30 14:20:38 +000092# Initialized in main()
93settings = None
94
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010095# Used by tests/git_cl_test.py to add extra logging.
96# Inside the weirdly failing test, add this:
97# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -070098# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010099_IS_BEING_TESTED = False
100
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000101
Christopher Lamf732cd52017-01-24 12:40:11 +1100102def DieWithError(message, change_desc=None):
103 if change_desc:
104 SaveDescriptionBackup(change_desc)
105
vapiera7fbd5a2016-06-16 09:17:49 -0700106 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107 sys.exit(1)
108
109
Christopher Lamf732cd52017-01-24 12:40:11 +1100110def SaveDescriptionBackup(change_desc):
111 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000112 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100113 backup_file = open(backup_path, 'w')
114 backup_file.write(change_desc.description)
115 backup_file.close()
116
117
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000118def GetNoGitPagerEnv():
119 env = os.environ.copy()
120 # 'cat' is a magical git string that disables pagers on all platforms.
121 env['GIT_PAGER'] = 'cat'
122 return env
123
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000124
bsep@chromium.org627d9002016-04-29 00:00:52 +0000125def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000126 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000127 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000128 except subprocess2.CalledProcessError as e:
129 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000130 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000132 'Command "%s" failed.\n%s' % (
133 ' '.join(args), error_message or e.stdout or ''))
134 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000135
136
137def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000138 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000139 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000140
141
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000142def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000143 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700144 if suppress_stderr:
145 stderr = subprocess2.VOID
146 else:
147 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000148 try:
tandrii5d48c322016-08-18 16:19:37 -0700149 (out, _), code = subprocess2.communicate(['git'] + args,
150 env=GetNoGitPagerEnv(),
151 stdout=subprocess2.PIPE,
152 stderr=stderr)
153 return code, out
154 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900155 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700156 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000157
158
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000159def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000160 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000161 return RunGitWithCode(args, suppress_stderr=True)[1]
162
163
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000164def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000165 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000166 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000167 return (version.startswith(prefix) and
168 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000169
170
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000171def BranchExists(branch):
172 """Return True if specified branch exists."""
173 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
174 suppress_stderr=True)
175 return not code
176
177
tandrii2a16b952016-10-19 07:09:44 -0700178def time_sleep(seconds):
179 # Use this so that it can be mocked in tests without interfering with python
180 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700181 return time.sleep(seconds)
182
183
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000184def time_time():
185 # Use this so that it can be mocked in tests without interfering with python
186 # system machinery.
187 return time.time()
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
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000685 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000686 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
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000725def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000726 """Checks if a yapf file is in any parent directory of fpath until top_dir.
727
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000728 Recursively checks parent directories to find yapf file and if no yapf file
729 is found returns None. Uses yapf_config_cache as a cache for
730 previously found configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000731 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000732 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000733 # Return result if we've already computed it.
734 if fpath in yapf_config_cache:
735 return yapf_config_cache[fpath]
736
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000737 parent_dir = os.path.dirname(fpath)
738 if os.path.isfile(fpath):
739 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000740 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000741 # Otherwise fpath is a directory
742 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
743 if os.path.isfile(yapf_file):
744 ret = yapf_file
745 elif fpath == top_dir or parent_dir == fpath:
746 # If we're at the top level directory, or if we're at root
747 # there is no provided style.
748 ret = None
749 else:
750 # Otherwise recurse on the current directory.
751 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000752 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):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000810 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 self.tree_status_url = None
812 self.viewvc_url = None
813 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000814 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000815 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000816 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000817 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818
819 def LazyUpdateIfNeeded(self):
820 """Updates the settings from a codereview.settings file, if available."""
821 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000822 # The only value that actually changes the behavior is
823 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000824 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000825 error_ok=True
826 ).strip().lower()
827
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000829 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000830 LoadCodereviewSettingsFromFile(cr_settings_file)
831 self.updated = True
832
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000833 @staticmethod
834 def GetRelativeRoot():
835 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000838 if self.root is None:
839 self.root = os.path.abspath(self.GetRelativeRoot())
840 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 def GetTreeStatusUrl(self, error_ok=False):
843 if not self.tree_status_url:
844 error_message = ('You must configure your tree status URL by running '
845 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000846 self.tree_status_url = self._GetConfig(
847 'rietveld.tree-status-url', error_ok=error_ok,
848 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 return self.tree_status_url
850
851 def GetViewVCUrl(self):
852 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000853 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 return self.viewvc_url
855
rmistry@google.com90752582014-01-14 21:04:50 +0000856 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000857 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000858
rmistry@google.com5626a922015-02-26 14:03:30 +0000859 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000860 run_post_upload_hook = self._GetConfig(
861 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000862 return run_post_upload_hook == "True"
863
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000864 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000865 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000866
ukai@chromium.orge8077812012-02-03 03:41:46 +0000867 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700868 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000869 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700870 self.is_gerrit = (
871 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000872 return self.is_gerrit
873
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000874 def GetSquashGerritUploads(self):
875 """Return true if uploads to Gerrit should be squashed by default."""
876 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700877 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
878 if self.squash_gerrit_uploads is None:
879 # Default is squash now (http://crbug.com/611892#c23).
880 self.squash_gerrit_uploads = not (
881 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
882 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000883 return self.squash_gerrit_uploads
884
tandriia60502f2016-06-20 02:01:53 -0700885 def GetSquashGerritUploadsOverride(self):
886 """Return True or False if codereview.settings should be overridden.
887
888 Returns None if no override has been defined.
889 """
890 # See also http://crbug.com/611892#c23
891 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
892 error_ok=True).strip()
893 if result == 'true':
894 return True
895 if result == 'false':
896 return False
897 return None
898
tandrii@chromium.org28253532016-04-14 13:46:56 +0000899 def GetGerritSkipEnsureAuthenticated(self):
900 """Return True if EnsureAuthenticated should not be done for Gerrit
901 uploads."""
902 if self.gerrit_skip_ensure_authenticated is None:
903 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000904 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000905 error_ok=True).strip() == 'true')
906 return self.gerrit_skip_ensure_authenticated
907
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000908 def GetGitEditor(self):
909 """Return the editor specified in the git config, or None if none is."""
910 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000911 # Git requires single quotes for paths with spaces. We need to replace
912 # them with double quotes for Windows to treat such paths as a single
913 # path.
914 self.git_editor = self._GetConfig(
915 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000916 return self.git_editor or None
917
thestig@chromium.org44202a22014-03-11 19:22:18 +0000918 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000919 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000920 DEFAULT_LINT_REGEX)
921
922 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000923 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000924 DEFAULT_LINT_IGNORE_REGEX)
925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 def _GetConfig(self, param, **kwargs):
927 self.LazyUpdateIfNeeded()
928 return RunGit(['config', param], **kwargs).strip()
929
930
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100931@contextlib.contextmanager
932def _get_gerrit_project_config_file(remote_url):
933 """Context manager to fetch and store Gerrit's project.config from
934 refs/meta/config branch and store it in temp file.
935
936 Provides a temporary filename or None if there was error.
937 """
938 error, _ = RunGitWithCode([
939 'fetch', remote_url,
940 '+refs/meta/config:refs/git_cl/meta/config'])
941 if error:
942 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700943 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100944 (remote_url, error))
945 yield None
946 return
947
948 error, project_config_data = RunGitWithCode(
949 ['show', 'refs/git_cl/meta/config:project.config'])
950 if error:
951 print('WARNING: project.config file not found')
952 yield None
953 return
954
955 with gclient_utils.temporary_directory() as tempdir:
956 project_config_file = os.path.join(tempdir, 'project.config')
957 gclient_utils.FileWrite(project_config_file, project_config_data)
958 yield project_config_file
959
960
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961def ShortBranchName(branch):
962 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000963 return branch.replace('refs/heads/', '', 1)
964
965
966def GetCurrentBranchRef():
967 """Returns branch ref (e.g., refs/heads/master) or None."""
968 return RunGit(['symbolic-ref', 'HEAD'],
969 stderr=subprocess2.VOID, error_ok=True).strip() or None
970
971
972def GetCurrentBranch():
973 """Returns current branch or None.
974
975 For refs/heads/* branches, returns just last part. For others, full ref.
976 """
977 branchref = GetCurrentBranchRef()
978 if branchref:
979 return ShortBranchName(branchref)
980 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981
982
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000983class _CQState(object):
984 """Enum for states of CL with respect to Commit Queue."""
985 NONE = 'none'
986 DRY_RUN = 'dry_run'
987 COMMIT = 'commit'
988
989 ALL_STATES = [NONE, DRY_RUN, COMMIT]
990
991
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000992class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200993 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000994 self.issue = issue
995 self.patchset = patchset
996 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +0000997 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200998 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000999
1000 @property
1001 def valid(self):
1002 return self.issue is not None
1003
1004
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001005def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001006 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1007 fail_result = _ParsedIssueNumberArgument()
1008
1009 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001010 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001011 if not arg.startswith('http'):
1012 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001013
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001014 url = gclient_utils.UpgradeToHttps(arg)
1015 try:
1016 parsed_url = urlparse.urlparse(url)
1017 except ValueError:
1018 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001019
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001020 if codereview is not None:
1021 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1022 return parsed or fail_result
1023
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001024 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001025
1026
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001027def _create_description_from_log(args):
1028 """Pulls out the commit log to use as a base for the CL description."""
1029 log_args = []
1030 if len(args) == 1 and not args[0].endswith('.'):
1031 log_args = [args[0] + '..']
1032 elif len(args) == 1 and args[0].endswith('...'):
1033 log_args = [args[0][:-1]]
1034 elif len(args) == 2:
1035 log_args = [args[0] + '..' + args[1]]
1036 else:
1037 log_args = args[:] # Hope for the best!
1038 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1039
1040
Aaron Gablea45ee112016-11-22 15:14:38 -08001041class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001042 def __init__(self, issue, url):
1043 self.issue = issue
1044 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001045 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001046
1047 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001048 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001049 self.issue, self.url)
1050
1051
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001052_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001053 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001054 # TODO(tandrii): these two aren't known in Gerrit.
1055 'approval', 'disapproval'])
1056
1057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001059 """Changelist works with one changelist in local branch.
1060
1061 Supports two codereview backends: Rietveld or Gerrit, selected at object
1062 creation.
1063
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001064 Notes:
1065 * Not safe for concurrent multi-{thread,process} use.
1066 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001067 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001068 """
1069
1070 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1071 """Create a new ChangeList instance.
1072
1073 If issue is given, the codereview must be given too.
1074
1075 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1076 Otherwise, it's decided based on current configuration of the local branch,
1077 with default being 'rietveld' for backwards compatibility.
1078 See _load_codereview_impl for more details.
1079
1080 **kwargs will be passed directly to codereview implementation.
1081 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001082 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001083 global settings
1084 if not settings:
1085 # Happens when git_cl.py is used as a utility library.
1086 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001087
1088 if issue:
1089 assert codereview, 'codereview must be known, if issue is known'
1090
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091 self.branchref = branchref
1092 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001093 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094 self.branch = ShortBranchName(self.branchref)
1095 else:
1096 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001098 self.lookedup_issue = False
1099 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 self.has_description = False
1101 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001102 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001104 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001105 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001106 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001107 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001108
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001109 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001110 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001111 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001112 assert self._codereview_impl
1113 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001114
1115 def _load_codereview_impl(self, codereview=None, **kwargs):
1116 if codereview:
Joe Masond87b0962018-12-03 21:04:46 +00001117 assert codereview in _CODEREVIEW_IMPLEMENTATIONS, (
1118 'codereview {} not in {}'.format(codereview,
1119 _CODEREVIEW_IMPLEMENTATIONS))
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001120 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1121 self._codereview = codereview
1122 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001123 return
1124
1125 # Automatic selection based on issue number set for a current branch.
1126 # Rietveld takes precedence over Gerrit.
1127 assert not self.issue
1128 # Whether we find issue or not, we are doing the lookup.
1129 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001130 if self.GetBranch():
1131 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1132 issue = _git_get_branch_config_value(
1133 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1134 if issue:
1135 self._codereview = codereview
1136 self._codereview_impl = cls(self, **kwargs)
1137 self.issue = int(issue)
1138 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001139
Bryce Thomascfc97122018-12-13 20:21:47 +00001140 # No issue is set for this branch, so default to gerrit.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001141 return self._load_codereview_impl(
Bryce Thomascfc97122018-12-13 20:21:47 +00001142 codereview='gerrit',
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001143 **kwargs)
1144
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001145 def IsGerrit(self):
1146 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001147
1148 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001149 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001150
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001151 The return value is a string suitable for passing to git cl with the --cc
1152 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153 """
1154 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001155 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001156 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001157 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1158 return self.cc
1159
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001160 def GetCCListWithoutDefault(self):
1161 """Return the users cc'd on this CL excluding default ones."""
1162 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001163 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001164 return self.cc
1165
Daniel Cheng7227d212017-11-17 08:12:37 -08001166 def ExtendCC(self, more_cc):
1167 """Extends the list of users to cc on this CL based on the changed files."""
1168 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169
1170 def GetBranch(self):
1171 """Returns the short branch name, e.g. 'master'."""
1172 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001173 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001174 if not branchref:
1175 return None
1176 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 self.branch = ShortBranchName(self.branchref)
1178 return self.branch
1179
1180 def GetBranchRef(self):
1181 """Returns the full branch name, e.g. 'refs/heads/master'."""
1182 self.GetBranch() # Poke the lazy loader.
1183 return self.branchref
1184
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001185 def ClearBranch(self):
1186 """Clears cached branch data of this object."""
1187 self.branch = self.branchref = None
1188
tandrii5d48c322016-08-18 16:19:37 -07001189 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1190 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1191 kwargs['branch'] = self.GetBranch()
1192 return _git_get_branch_config_value(key, default, **kwargs)
1193
1194 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1195 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1196 assert self.GetBranch(), (
1197 'this CL must have an associated branch to %sset %s%s' %
1198 ('un' if value is None else '',
1199 key,
1200 '' if value is None else ' to %r' % value))
1201 kwargs['branch'] = self.GetBranch()
1202 return _git_set_branch_config_value(key, value, **kwargs)
1203
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001204 @staticmethod
1205 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001206 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 e.g. 'origin', 'refs/heads/master'
1208 """
1209 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001210 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1211
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001213 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001215 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1216 error_ok=True).strip()
1217 if upstream_branch:
1218 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001219 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001220 # Else, try to guess the origin remote.
1221 remote_branches = RunGit(['branch', '-r']).split()
1222 if 'origin/master' in remote_branches:
1223 # Fall back on origin/master if it exits.
1224 remote = 'origin'
1225 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001227 DieWithError(
1228 'Unable to determine default branch to diff against.\n'
1229 'Either pass complete "git diff"-style arguments, like\n'
1230 ' git cl upload origin/master\n'
1231 'or verify this branch is set up to track another \n'
1232 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233
1234 return remote, upstream_branch
1235
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001236 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001237 upstream_branch = self.GetUpstreamBranch()
1238 if not BranchExists(upstream_branch):
1239 DieWithError('The upstream for the current branch (%s) does not exist '
1240 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001241 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001242 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001243
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244 def GetUpstreamBranch(self):
1245 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001246 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001248 upstream_branch = upstream_branch.replace('refs/heads/',
1249 'refs/remotes/%s/' % remote)
1250 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1251 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 self.upstream_branch = upstream_branch
1253 return self.upstream_branch
1254
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001255 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001256 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001257 remote, branch = None, self.GetBranch()
1258 seen_branches = set()
1259 while branch not in seen_branches:
1260 seen_branches.add(branch)
1261 remote, branch = self.FetchUpstreamTuple(branch)
1262 branch = ShortBranchName(branch)
1263 if remote != '.' or branch.startswith('refs/remotes'):
1264 break
1265 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001266 remotes = RunGit(['remote'], error_ok=True).split()
1267 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001268 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001269 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001271 logging.warn('Could not determine which remote this change is '
1272 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001273 else:
1274 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001275 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001276 branch = 'HEAD'
1277 if branch.startswith('refs/remotes'):
1278 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001279 elif branch.startswith('refs/branch-heads/'):
1280 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001281 else:
1282 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001283 return self._remote
1284
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001285 def GitSanityChecks(self, upstream_git_obj):
1286 """Checks git repo status and ensures diff is from local commits."""
1287
sbc@chromium.org79706062015-01-14 21:18:12 +00001288 if upstream_git_obj is None:
1289 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001290 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001291 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001292 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001293 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001294 return False
1295
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001296 # Verify the commit we're diffing against is in our current branch.
1297 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1298 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1299 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001300 print('ERROR: %s is not in the current branch. You may need to rebase '
1301 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001302 return False
1303
1304 # List the commits inside the diff, and verify they are all local.
1305 commits_in_diff = RunGit(
1306 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1307 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1308 remote_branch = remote_branch.strip()
1309 if code != 0:
1310 _, remote_branch = self.GetRemoteBranch()
1311
1312 commits_in_remote = RunGit(
1313 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1314
1315 common_commits = set(commits_in_diff) & set(commits_in_remote)
1316 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001317 print('ERROR: Your diff contains %d commits already in %s.\n'
1318 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1319 'the diff. If you are using a custom git flow, you can override'
1320 ' the reference used for this check with "git config '
1321 'gitcl.remotebranch <git-ref>".' % (
1322 len(common_commits), remote_branch, upstream_git_obj),
1323 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001324 return False
1325 return True
1326
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001327 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001328 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001329
1330 Returns None if it is not set.
1331 """
tandrii5d48c322016-08-18 16:19:37 -07001332 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001333
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334 def GetRemoteUrl(self):
1335 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1336
1337 Returns None if there is no remote.
1338 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001339 is_cached, value = self._cached_remote_url
1340 if is_cached:
1341 return value
1342
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001343 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001344 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1345
Edward Lemur298f2cf2019-02-22 21:40:39 +00001346 # Check if the remote url can be parsed as an URL.
1347 host = urlparse.urlparse(url).netloc
1348 if host:
1349 self._cached_remote_url = (True, url)
1350 return url
1351
1352 # If it cannot be parsed as an url, assume it is a local directory, probably
1353 # a git cache.
1354 logging.warning('"%s" doesn\'t appear to point to a git host. '
1355 'Interpreting it as a local directory.', url)
1356 if not os.path.isdir(url):
1357 logging.error(
1358 'Remote "%s" for branch "%s" points to "%s", but it doesn\'t exist.',
1359 remote, url, self.GetBranch())
1360 return None
1361
1362 cache_path = url
1363 url = RunGit(['config', 'remote.%s.url' % remote],
1364 error_ok=True,
1365 cwd=url).strip()
1366
1367 host = urlparse.urlparse(url).netloc
1368 if not host:
1369 logging.error(
1370 'Remote "%(remote)s" for branch "%(branch)s" points to '
1371 '"%(cache_path)s", but it is misconfigured.\n'
1372 '"%(cache_path)s" must be a git repo and must have a remote named '
1373 '"%(remote)s" pointing to the git host.', {
1374 'remote': remote,
1375 'cache_path': cache_path,
1376 'branch': self.GetBranch()})
1377 return None
1378
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001379 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001380 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001382 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001383 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001384 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001385 self.issue = self._GitGetBranchConfigValue(
1386 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001387 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388 return self.issue
1389
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390 def GetIssueURL(self):
1391 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001392 issue = self.GetIssue()
1393 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001394 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001395 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001397 def GetDescription(self, pretty=False, force=False):
1398 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001400 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 self.has_description = True
1402 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001403 # Set width to 72 columns + 2 space indent.
1404 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001406 lines = self.description.splitlines()
1407 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 return self.description
1409
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001410 def GetDescriptionFooters(self):
1411 """Returns (non_footer_lines, footers) for the commit message.
1412
1413 Returns:
1414 non_footer_lines (list(str)) - Simple list of description lines without
1415 any footer. The lines do not contain newlines, nor does the list contain
1416 the empty line between the message and the footers.
1417 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1418 [("Change-Id", "Ideadbeef...."), ...]
1419 """
1420 raw_description = self.GetDescription()
1421 msg_lines, _, footers = git_footers.split_footers(raw_description)
1422 if footers:
1423 msg_lines = msg_lines[:len(msg_lines)-1]
1424 return msg_lines, footers
1425
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001427 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001428 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001429 self.patchset = self._GitGetBranchConfigValue(
1430 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001431 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001432 return self.patchset
1433
1434 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001435 """Set this branch's patchset. If patchset=0, clears the patchset."""
1436 assert self.GetBranch()
1437 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001438 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001439 else:
1440 self.patchset = int(patchset)
1441 self._GitSetBranchConfigValue(
1442 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001444 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001445 """Set this branch's issue. If issue isn't given, clears the issue."""
1446 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001448 issue = int(issue)
1449 self._GitSetBranchConfigValue(
1450 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001451 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001452 codereview_server = self._codereview_impl.GetCodereviewServer()
1453 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001454 self._GitSetBranchConfigValue(
1455 self._codereview_impl.CodereviewServerConfigKey(),
1456 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457 else:
tandrii5d48c322016-08-18 16:19:37 -07001458 # Reset all of these just to be clean.
1459 reset_suffixes = [
1460 'last-upload-hash',
1461 self._codereview_impl.IssueConfigKey(),
1462 self._codereview_impl.PatchsetConfigKey(),
1463 self._codereview_impl.CodereviewServerConfigKey(),
1464 ] + self._PostUnsetIssueProperties()
1465 for prop in reset_suffixes:
1466 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001467 msg = RunGit(['log', '-1', '--format=%B']).strip()
1468 if msg and git_footers.get_footer_change_id(msg):
1469 print('WARNING: The change patched into this branch has a Change-Id. '
1470 'Removing it.')
1471 RunGit(['commit', '--amend', '-m',
1472 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001473 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001474 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475
dnjba1b0f32016-09-02 12:37:42 -07001476 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001477 if not self.GitSanityChecks(upstream_branch):
1478 DieWithError('\nGit sanity check failure')
1479
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001480 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001481 if not root:
1482 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001483 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001484
1485 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001486 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001487 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001488 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001489 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001490 except subprocess2.CalledProcessError:
1491 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001492 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001493 'This branch probably doesn\'t exist anymore. To reset the\n'
1494 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001495 ' git branch --set-upstream-to origin/master %s\n'
1496 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001497 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001498
maruel@chromium.org52424302012-08-29 15:14:30 +00001499 issue = self.GetIssue()
1500 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001501 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001502 description = self.GetDescription()
1503 else:
1504 # If the change was never uploaded, use the log messages of all commits
1505 # up to the branch point, as git cl upload will prefill the description
1506 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001507 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1508 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001509
1510 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001511 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001512 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001513 name,
1514 description,
1515 absroot,
1516 files,
1517 issue,
1518 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001519 author,
1520 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001521
dsansomee2d6fd92016-09-08 00:10:47 -07001522 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001523 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001524 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001525 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001526
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001527 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1528 """Sets the description for this CL remotely.
1529
1530 You can get description_lines and footers with GetDescriptionFooters.
1531
1532 Args:
1533 description_lines (list(str)) - List of CL description lines without
1534 newline characters.
1535 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1536 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1537 `List-Of-Tokens`). It will be case-normalized so that each token is
1538 title-cased.
1539 """
1540 new_description = '\n'.join(description_lines)
1541 if footers:
1542 new_description += '\n'
1543 for k, v in footers:
1544 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1545 if not git_footers.FOOTER_PATTERN.match(foot):
1546 raise ValueError('Invalid footer %r' % foot)
1547 new_description += foot + '\n'
1548 self.UpdateDescription(new_description, force)
1549
Edward Lesmes8e282792018-04-03 18:50:29 -04001550 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001551 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1552 try:
1553 return presubmit_support.DoPresubmitChecks(change, committing,
1554 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1555 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001556 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1557 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001558 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001559 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001560
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001561 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1562 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001563 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1564 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001565 else:
1566 # Assume url.
1567 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1568 urlparse.urlparse(issue_arg))
1569 if not parsed_issue_arg or not parsed_issue_arg.valid:
1570 DieWithError('Failed to parse issue argument "%s". '
1571 'Must be an issue number or a valid URL.' % issue_arg)
1572 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001573 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001574
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001575 def CMDUpload(self, options, git_diff_args, orig_args):
1576 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001577 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001578 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001579 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001580 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001581 else:
1582 if self.GetBranch() is None:
1583 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1584
1585 # Default to diffing against common ancestor of upstream branch
1586 base_branch = self.GetCommonAncestorWithUpstream()
1587 git_diff_args = [base_branch, 'HEAD']
1588
Aaron Gablec4c40d12017-05-22 11:49:53 -07001589
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001590 # Fast best-effort checks to abort before running potentially
1591 # expensive hooks if uploading is likely to fail anyway. Passing these
1592 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001593 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001594 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001595
1596 # Apply watchlists on upload.
1597 change = self.GetChange(base_branch, None)
1598 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1599 files = [f.LocalPath() for f in change.AffectedFiles()]
1600 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001601 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001602
1603 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001604 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001605 # Set the reviewer list now so that presubmit checks can access it.
1606 change_description = ChangeDescription(change.FullDescriptionText())
1607 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001608 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001609 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001610 change)
1611 change.SetDescriptionText(change_description.description)
1612 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001613 may_prompt=not options.force,
1614 verbose=options.verbose,
1615 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001616 if not hook_results.should_continue():
1617 return 1
1618 if not options.reviewers and hook_results.reviewers:
1619 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001620 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001621
Aaron Gable13101a62018-02-09 13:20:41 -08001622 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001623 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001624 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001625 _git_set_branch_config_value('last-upload-hash',
1626 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001627 # Run post upload hooks, if specified.
1628 if settings.GetRunPostUploadHook():
1629 presubmit_support.DoPostUploadExecuter(
1630 change,
1631 self,
1632 settings.GetRoot(),
1633 options.verbose,
1634 sys.stdout)
1635
1636 # Upload all dependencies if specified.
1637 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001638 print()
1639 print('--dependencies has been specified.')
1640 print('All dependent local branches will be re-uploaded.')
1641 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001642 # Remove the dependencies flag from args so that we do not end up in a
1643 # loop.
1644 orig_args.remove('--dependencies')
1645 ret = upload_branch_deps(self, orig_args)
1646 return ret
1647
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001648 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001649 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001650
1651 Issue must have been already uploaded and known.
1652 """
1653 assert new_state in _CQState.ALL_STATES
1654 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001655 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001656 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001657 return 0
1658 except KeyboardInterrupt:
1659 raise
1660 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001661 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001662 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001663 ' * Your project has no CQ,\n'
1664 ' * You don\'t have permission to change the CQ state,\n'
1665 ' * There\'s a bug in this code (see stack trace below).\n'
1666 'Consider specifying which bots to trigger manually or asking your '
1667 'project owners for permissions or contacting Chrome Infra at:\n'
1668 'https://www.chromium.org/infra\n\n' %
1669 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001670 # Still raise exception so that stack trace is printed.
1671 raise
1672
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001673 # Forward methods to codereview specific implementation.
1674
Aaron Gable636b13f2017-07-14 10:42:48 -07001675 def AddComment(self, message, publish=None):
1676 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001677
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001678 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001679 """Returns list of _CommentSummary for each comment.
1680
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001681 args:
1682 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001683 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001684 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001685
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001686 def CloseIssue(self):
1687 return self._codereview_impl.CloseIssue()
1688
1689 def GetStatus(self):
1690 return self._codereview_impl.GetStatus()
1691
1692 def GetCodereviewServer(self):
1693 return self._codereview_impl.GetCodereviewServer()
1694
tandriide281ae2016-10-12 06:02:30 -07001695 def GetIssueOwner(self):
1696 """Get owner from codereview, which may differ from this checkout."""
1697 return self._codereview_impl.GetIssueOwner()
1698
Edward Lemur707d70b2018-02-07 00:50:14 +01001699 def GetReviewers(self):
1700 return self._codereview_impl.GetReviewers()
1701
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001702 def GetMostRecentPatchset(self):
1703 return self._codereview_impl.GetMostRecentPatchset()
1704
tandriide281ae2016-10-12 06:02:30 -07001705 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001706 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001707 return self._codereview_impl.CannotTriggerTryJobReason()
1708
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001709 def GetTryJobProperties(self, patchset=None):
1710 """Returns dictionary of properties to launch try job."""
1711 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001712
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001713 def __getattr__(self, attr):
1714 # This is because lots of untested code accesses Rietveld-specific stuff
1715 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001716 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001717 # Note that child method defines __getattr__ as well, and forwards it here,
1718 # because _RietveldChangelistImpl is not cleaned up yet, and given
1719 # deprecation of Rietveld, it should probably be just removed.
1720 # Until that time, avoid infinite recursion by bypassing __getattr__
1721 # of implementation class.
1722 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001723
1724
1725class _ChangelistCodereviewBase(object):
1726 """Abstract base class encapsulating codereview specifics of a changelist."""
1727 def __init__(self, changelist):
1728 self._changelist = changelist # instance of Changelist
1729
1730 def __getattr__(self, attr):
1731 # Forward methods to changelist.
1732 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1733 # _RietveldChangelistImpl to avoid this hack?
1734 return getattr(self._changelist, attr)
1735
1736 def GetStatus(self):
1737 """Apply a rough heuristic to give a simple summary of an issue's review
1738 or CQ status, assuming adherence to a common workflow.
1739
1740 Returns None if no issue for this branch, or specific string keywords.
1741 """
1742 raise NotImplementedError()
1743
1744 def GetCodereviewServer(self):
1745 """Returns server URL without end slash, like "https://codereview.com"."""
1746 raise NotImplementedError()
1747
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001748 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001749 """Fetches and returns description from the codereview server."""
1750 raise NotImplementedError()
1751
tandrii5d48c322016-08-18 16:19:37 -07001752 @classmethod
1753 def IssueConfigKey(cls):
1754 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001755 raise NotImplementedError()
1756
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001757 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001758 def PatchsetConfigKey(cls):
1759 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001760 raise NotImplementedError()
1761
tandrii5d48c322016-08-18 16:19:37 -07001762 @classmethod
1763 def CodereviewServerConfigKey(cls):
1764 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001765 raise NotImplementedError()
1766
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001767 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001768 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001769 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001770
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001771 def GetGerritObjForPresubmit(self):
1772 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1773 return None
1774
dsansomee2d6fd92016-09-08 00:10:47 -07001775 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001776 """Update the description on codereview site."""
1777 raise NotImplementedError()
1778
Aaron Gable636b13f2017-07-14 10:42:48 -07001779 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001780 """Posts a comment to the codereview site."""
1781 raise NotImplementedError()
1782
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001783 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001784 raise NotImplementedError()
1785
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001786 def CloseIssue(self):
1787 """Closes the issue."""
1788 raise NotImplementedError()
1789
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001790 def GetMostRecentPatchset(self):
1791 """Returns the most recent patchset number from the codereview site."""
1792 raise NotImplementedError()
1793
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001794 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001795 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001796 """Fetches and applies the issue.
1797
1798 Arguments:
1799 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1800 reject: if True, reject the failed patch instead of switching to 3-way
1801 merge. Rietveld only.
1802 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1803 only.
1804 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001805 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001806 """
1807 raise NotImplementedError()
1808
1809 @staticmethod
1810 def ParseIssueURL(parsed_url):
1811 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1812 failed."""
1813 raise NotImplementedError()
1814
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001815 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001816 """Best effort check that user is authenticated with codereview server.
1817
1818 Arguments:
1819 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001820 refresh: whether to attempt to refresh credentials. Ignored if not
1821 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001822 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001823 raise NotImplementedError()
1824
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001825 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001826 """Best effort check that uploading isn't supposed to fail for predictable
1827 reasons.
1828
1829 This method should raise informative exception if uploading shouldn't
1830 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001831
1832 Arguments:
1833 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001834 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001835 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001836
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001837 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001838 """Uploads a change to codereview."""
1839 raise NotImplementedError()
1840
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001841 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001842 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001843
1844 Issue must have been already uploaded and known.
1845 """
1846 raise NotImplementedError()
1847
tandriie113dfd2016-10-11 10:20:12 -07001848 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001849 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001850 raise NotImplementedError()
1851
tandriide281ae2016-10-12 06:02:30 -07001852 def GetIssueOwner(self):
1853 raise NotImplementedError()
1854
Edward Lemur707d70b2018-02-07 00:50:14 +01001855 def GetReviewers(self):
1856 raise NotImplementedError()
1857
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001858 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001859 raise NotImplementedError()
1860
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001861
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001862class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001863 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001864 # auth_config is Rietveld thing, kept here to preserve interface only.
1865 super(_GerritChangelistImpl, self).__init__(changelist)
1866 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001867 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001868 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001869 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001870 # Map from change number (issue) to its detail cache.
1871 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001872
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001873 if codereview_host is not None:
1874 assert not codereview_host.startswith('https://'), codereview_host
1875 self._gerrit_host = codereview_host
1876 self._gerrit_server = 'https://%s' % codereview_host
1877
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001878 def _GetGerritHost(self):
1879 # Lazy load of configs.
1880 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001881 if self._gerrit_host and '.' not in self._gerrit_host:
1882 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1883 # This happens for internal stuff http://crbug.com/614312.
1884 parsed = urlparse.urlparse(self.GetRemoteUrl())
1885 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001886 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07001887 ' Your current remote is: %s' % self.GetRemoteUrl())
1888 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1889 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001890 return self._gerrit_host
1891
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001892 def _GetGitHost(self):
1893 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001894 remote_url = self.GetRemoteUrl()
1895 if not remote_url:
1896 return None
1897 return urlparse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001898
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001899 def GetCodereviewServer(self):
1900 if not self._gerrit_server:
1901 # If we're on a branch then get the server potentially associated
1902 # with that branch.
1903 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001904 self._gerrit_server = self._GitGetBranchConfigValue(
1905 self.CodereviewServerConfigKey())
1906 if self._gerrit_server:
1907 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001908 if not self._gerrit_server:
1909 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1910 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001911 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001912 parts[0] = parts[0] + '-review'
1913 self._gerrit_host = '.'.join(parts)
1914 self._gerrit_server = 'https://%s' % self._gerrit_host
1915 return self._gerrit_server
1916
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001917 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001918 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001919 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001920 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001921 logging.warn('can\'t detect Gerrit project.')
1922 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001923 project = urlparse.urlparse(remote_url).path.strip('/')
1924 if project.endswith('.git'):
1925 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001926 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1927 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1928 # gitiles/git-over-https protocol. E.g.,
1929 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1930 # as
1931 # https://chromium.googlesource.com/v8/v8
1932 if project.startswith('a/'):
1933 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001934 return project
1935
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001936 def _GerritChangeIdentifier(self):
1937 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1938
1939 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001940 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001941 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001942 project = self._GetGerritProject()
1943 if project:
1944 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1945 # Fall back on still unique, but less efficient change number.
1946 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001947
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001948 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001949 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001950 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001951
tandrii5d48c322016-08-18 16:19:37 -07001952 @classmethod
1953 def PatchsetConfigKey(cls):
1954 return 'gerritpatchset'
1955
1956 @classmethod
1957 def CodereviewServerConfigKey(cls):
1958 return 'gerritserver'
1959
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001960 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001961 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001962 if settings.GetGerritSkipEnsureAuthenticated():
1963 # For projects with unusual authentication schemes.
1964 # See http://crbug.com/603378.
1965 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001966
1967 # Check presence of cookies only if using cookies-based auth method.
1968 cookie_auth = gerrit_util.Authenticator.get()
1969 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001970 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001971
1972 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001973 self.GetCodereviewServer()
1974 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001975 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001976
1977 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1978 git_auth = cookie_auth.get_auth_header(git_host)
1979 if gerrit_auth and git_auth:
1980 if gerrit_auth == git_auth:
1981 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001982 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001983 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001984 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001985 ' %s\n'
1986 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001987 ' Consider running the following command:\n'
1988 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001989 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02001990 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001991 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001992 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001993 cookie_auth.get_new_password_message(git_host)))
1994 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001995 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001996 return
1997 else:
1998 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001999 ([] if gerrit_auth else [self._gerrit_host]) +
2000 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002001 DieWithError('Credentials for the following hosts are required:\n'
2002 ' %s\n'
2003 'These are read from %s (or legacy %s)\n'
2004 '%s' % (
2005 '\n '.join(missing),
2006 cookie_auth.get_gitcookies_path(),
2007 cookie_auth.get_netrc_path(),
2008 cookie_auth.get_new_password_message(git_host)))
2009
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002010 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002011 if not self.GetIssue():
2012 return
2013
2014 # Warm change details cache now to avoid RPCs later, reducing latency for
2015 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002016 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002017 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002018
2019 status = self._GetChangeDetail()['status']
2020 if status in ('MERGED', 'ABANDONED'):
2021 DieWithError('Change %s has been %s, new uploads are not allowed' %
2022 (self.GetIssueURL(),
2023 'submitted' if status == 'MERGED' else 'abandoned'))
2024
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002025 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2026 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2027 # Apparently this check is not very important? Otherwise get_auth_email
2028 # could have been added to other implementations of Authenticator.
2029 cookies_auth = gerrit_util.Authenticator.get()
2030 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002031 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002032
2033 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002034 if self.GetIssueOwner() == cookies_user:
2035 return
2036 logging.debug('change %s owner is %s, cookies user is %s',
2037 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002038 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002039 # so ask what Gerrit thinks of this user.
2040 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2041 if details['email'] == self.GetIssueOwner():
2042 return
2043 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002044 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002045 'as %s.\n'
2046 'Uploading may fail due to lack of permissions.' %
2047 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2048 confirm_or_exit(action='upload')
2049
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002050 def _PostUnsetIssueProperties(self):
2051 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002052 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002053
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002054 def GetGerritObjForPresubmit(self):
2055 return presubmit_support.GerritAccessor(self._GetGerritHost())
2056
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002057 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002058 """Apply a rough heuristic to give a simple summary of an issue's review
2059 or CQ status, assuming adherence to a common workflow.
2060
2061 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002062 * 'error' - error from review tool (including deleted issues)
2063 * 'unsent' - no reviewers added
2064 * 'waiting' - waiting for review
2065 * 'reply' - waiting for uploader to reply to review
2066 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002067 * 'dry-run' - dry-running in the commit queue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002068 * 'commit' - in the commit queue
2069 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002070 """
2071 if not self.GetIssue():
2072 return None
2073
2074 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002075 data = self._GetChangeDetail([
2076 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002077 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002078 return 'error'
2079
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002080 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002081 return 'closed'
2082
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002083 cq_label = data['labels'].get('Commit-Queue', {})
2084 max_cq_vote = 0
2085 for vote in cq_label.get('all', []):
2086 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2087 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002088 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002089 if max_cq_vote == 1:
2090 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002091
Aaron Gable9ab38c62017-04-06 14:36:33 -07002092 if data['labels'].get('Code-Review', {}).get('approved'):
2093 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002094
2095 if not data.get('reviewers', {}).get('REVIEWER', []):
2096 return 'unsent'
2097
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002098 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002099 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2100 last_message_author = messages.pop().get('author', {})
2101 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002102 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2103 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002104 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002105 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002106 if last_message_author.get('_account_id') == owner:
2107 # Most recent message was by owner.
2108 return 'waiting'
2109 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002110 # Some reply from non-owner.
2111 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002112
2113 # Somehow there are no messages even though there are reviewers.
2114 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002115
2116 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002117 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002118 patchset = data['revisions'][data['current_revision']]['_number']
2119 self.SetPatchset(patchset)
2120 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002121
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002122 def FetchDescription(self, force=False):
2123 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2124 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002125 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002126 return data['revisions'][current_rev]['commit']['message'].encode(
2127 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002128
dsansomee2d6fd92016-09-08 00:10:47 -07002129 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002130 if gerrit_util.HasPendingChangeEdit(
2131 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002132 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002133 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002134 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002135 'unpublished edit. Either publish the edit in the Gerrit web UI '
2136 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002137
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002138 gerrit_util.DeletePendingChangeEdit(
2139 self._GetGerritHost(), self._GerritChangeIdentifier())
2140 gerrit_util.SetCommitMessage(
2141 self._GetGerritHost(), self._GerritChangeIdentifier(),
2142 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002143
Aaron Gable636b13f2017-07-14 10:42:48 -07002144 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002145 gerrit_util.SetReview(
2146 self._GetGerritHost(), self._GerritChangeIdentifier(),
2147 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002148
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002149 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002150 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002151 # CURRENT_REVISION is included to get the latest patchset so that
2152 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002153 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002154 options=['MESSAGES', 'DETAILED_ACCOUNTS',
2155 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002156 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002157 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002158 robot_file_comments = gerrit_util.GetChangeRobotComments(
2159 self._GetGerritHost(), self._GerritChangeIdentifier())
2160
2161 # Add the robot comments onto the list of comments, but only
2162 # keep those that are from the latest pachset.
2163 latest_patch_set = self.GetMostRecentPatchset()
2164 for path, robot_comments in robot_file_comments.iteritems():
2165 line_comments = file_comments.setdefault(path, [])
2166 line_comments.extend(
2167 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002168
2169 # Build dictionary of file comments for easy access and sorting later.
2170 # {author+date: {path: {patchset: {line: url+message}}}}
2171 comments = collections.defaultdict(
2172 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2173 for path, line_comments in file_comments.iteritems():
2174 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002175 tag = comment.get('tag', '')
2176 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002177 continue
2178 key = (comment['author']['email'], comment['updated'])
2179 if comment.get('side', 'REVISION') == 'PARENT':
2180 patchset = 'Base'
2181 else:
2182 patchset = 'PS%d' % comment['patch_set']
2183 line = comment.get('line', 0)
2184 url = ('https://%s/c/%s/%s/%s#%s%s' %
2185 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2186 'b' if comment.get('side') == 'PARENT' else '',
2187 str(line) if line else ''))
2188 comments[key][path][patchset][line] = (url, comment['message'])
2189
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002190 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002191 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002192 summary = self._BuildCommentSummary(msg, comments, readable)
2193 if summary:
2194 summaries.append(summary)
2195 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002196
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002197 @staticmethod
2198 def _BuildCommentSummary(msg, comments, readable):
2199 key = (msg['author']['email'], msg['date'])
2200 # Don't bother showing autogenerated messages that don't have associated
2201 # file or line comments. this will filter out most autogenerated
2202 # messages, but will keep robot comments like those from Tricium.
2203 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2204 if is_autogenerated and not comments.get(key):
2205 return None
2206 message = msg['message']
2207 # Gerrit spits out nanoseconds.
2208 assert len(msg['date'].split('.')[-1]) == 9
2209 date = datetime.datetime.strptime(msg['date'][:-3],
2210 '%Y-%m-%d %H:%M:%S.%f')
2211 if key in comments:
2212 message += '\n'
2213 for path, patchsets in sorted(comments.get(key, {}).items()):
2214 if readable:
2215 message += '\n%s' % path
2216 for patchset, lines in sorted(patchsets.items()):
2217 for line, (url, content) in sorted(lines.items()):
2218 if line:
2219 line_str = 'Line %d' % line
2220 path_str = '%s:%d:' % (path, line)
2221 else:
2222 line_str = 'File comment'
2223 path_str = '%s:0:' % path
2224 if readable:
2225 message += '\n %s, %s: %s' % (patchset, line_str, url)
2226 message += '\n %s\n' % content
2227 else:
2228 message += '\n%s ' % path_str
2229 message += '\n%s\n' % content
2230
2231 return _CommentSummary(
2232 date=date,
2233 message=message,
2234 sender=msg['author']['email'],
2235 autogenerated=is_autogenerated,
2236 # These could be inferred from the text messages and correlated with
2237 # Code-Review label maximum, however this is not reliable.
2238 # Leaving as is until the need arises.
2239 approval=False,
2240 disapproval=False,
2241 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002242
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002243 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002244 gerrit_util.AbandonChange(
2245 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002246
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002247 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002248 gerrit_util.SubmitChange(
2249 self._GetGerritHost(), self._GerritChangeIdentifier(),
2250 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002251
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002252 def _GetChangeDetail(self, options=None, no_cache=False):
2253 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002254
2255 If fresh data is needed, set no_cache=True which will clear cache and
2256 thus new data will be fetched from Gerrit.
2257 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002258 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002259 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002260
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002261 # Optimization to avoid multiple RPCs:
2262 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2263 'CURRENT_COMMIT' not in options):
2264 options.append('CURRENT_COMMIT')
2265
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002266 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002267 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002268 options = [o.upper() for o in options]
2269
2270 # Check in cache first unless no_cache is True.
2271 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002272 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002273 else:
2274 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002275 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002276 # Assumption: data fetched before with extra options is suitable
2277 # for return for a smaller set of options.
2278 # For example, if we cached data for
2279 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2280 # and request is for options=[CURRENT_REVISION],
2281 # THEN we can return prior cached data.
2282 if options_set.issubset(cached_options_set):
2283 return data
2284
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002285 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002286 data = gerrit_util.GetChangeDetail(
2287 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002288 except gerrit_util.GerritError as e:
2289 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002290 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002291 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002292
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002293 self._detail_cache.setdefault(cache_key, []).append(
2294 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002295 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002296
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002297 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002298 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002299 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002300 data = gerrit_util.GetChangeCommit(
2301 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002302 except gerrit_util.GerritError as e:
2303 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002304 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002305 raise
agable32978d92016-11-01 12:55:02 -07002306 return data
2307
Karen Qian40c19422019-03-13 21:28:29 +00002308 def _IsCqConfigured(self):
2309 detail = self._GetChangeDetail(['LABELS'])
2310 if not u'Commit-Queue' in detail.get('labels', {}):
2311 return False
2312 # TODO(crbug/753213): Remove temporary hack
2313 if ('https://chromium.googlesource.com/chromium/src' ==
2314 self._changelist.GetRemoteUrl() and
2315 detail['branch'].startswith('refs/branch-heads/')):
2316 return False
2317 return True
2318
Olivier Robin75ee7252018-04-13 10:02:56 +02002319 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002320 if git_common.is_dirty_git_tree('land'):
2321 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002322
tandriid60367b2016-06-22 05:25:12 -07002323 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002324 if not force and self._IsCqConfigured():
2325 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002326 'which can test and land changes for you. '
2327 'Are you sure you wish to bypass it?\n',
2328 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002329 differs = True
tandriic4344b52016-08-29 06:04:54 -07002330 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002331 # Note: git diff outputs nothing if there is no diff.
2332 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002333 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002334 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002335 if detail['current_revision'] == last_upload:
2336 differs = False
2337 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002338 print('WARNING: Local branch contents differ from latest uploaded '
2339 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002340 if differs:
2341 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002342 confirm_or_exit(
2343 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2344 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002345 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002346 elif not bypass_hooks:
2347 hook_results = self.RunHook(
2348 committing=True,
2349 may_prompt=not force,
2350 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002351 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2352 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002353 if not hook_results.should_continue():
2354 return 1
2355
2356 self.SubmitIssue(wait_for_merge=True)
2357 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002358 links = self._GetChangeCommit().get('web_links', [])
2359 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002360 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002361 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002362 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002363 return 0
2364
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002365 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002366 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002367 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002368 assert not directory
2369 assert parsed_issue_arg.valid
2370
2371 self._changelist.issue = parsed_issue_arg.issue
2372
2373 if parsed_issue_arg.hostname:
2374 self._gerrit_host = parsed_issue_arg.hostname
2375 self._gerrit_server = 'https://%s' % self._gerrit_host
2376
tandriic2405f52016-10-10 08:13:15 -07002377 try:
2378 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002379 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002380 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002381
2382 if not parsed_issue_arg.patchset:
2383 # Use current revision by default.
2384 revision_info = detail['revisions'][detail['current_revision']]
2385 patchset = int(revision_info['_number'])
2386 else:
2387 patchset = parsed_issue_arg.patchset
2388 for revision_info in detail['revisions'].itervalues():
2389 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2390 break
2391 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002392 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002393 (parsed_issue_arg.patchset, self.GetIssue()))
2394
Aaron Gable697a91b2018-01-19 15:20:15 -08002395 remote_url = self._changelist.GetRemoteUrl()
2396 if remote_url.endswith('.git'):
2397 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002398 remote_url = remote_url.rstrip('/')
2399
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002400 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002401 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002402
2403 if remote_url != fetch_info['url']:
2404 DieWithError('Trying to patch a change from %s but this repo appears '
2405 'to be %s.' % (fetch_info['url'], remote_url))
2406
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002407 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002408
Aaron Gable62619a32017-06-16 08:22:09 -07002409 if force:
2410 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2411 print('Checked out commit for change %i patchset %i locally' %
2412 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002413 elif nocommit:
2414 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2415 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002416 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002417 RunGit(['cherry-pick', 'FETCH_HEAD'])
2418 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002419 (parsed_issue_arg.issue, patchset))
2420 print('Note: this created a local commit which does not have '
2421 'the same hash as the one uploaded for review. This will make '
2422 'uploading changes based on top of this branch difficult.\n'
2423 'If you want to do that, use "git cl patch --force" instead.')
2424
Stefan Zagerd08043c2017-10-12 12:07:02 -07002425 if self.GetBranch():
2426 self.SetIssue(parsed_issue_arg.issue)
2427 self.SetPatchset(patchset)
2428 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2429 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2430 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2431 else:
2432 print('WARNING: You are in detached HEAD state.\n'
2433 'The patch has been applied to your checkout, but you will not be '
2434 'able to upload a new patch set to the gerrit issue.\n'
2435 'Try using the \'-b\' option if you would like to work on a '
2436 'branch and/or upload a new patch set.')
2437
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002438 return 0
2439
2440 @staticmethod
2441 def ParseIssueURL(parsed_url):
2442 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2443 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002444 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2445 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002446 # Short urls like https://domain/<issue_number> can be used, but don't allow
2447 # specifying the patchset (you'd 404), but we allow that here.
2448 if parsed_url.path == '/':
2449 part = parsed_url.fragment
2450 else:
2451 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002452 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002453 if match:
2454 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002455 issue=int(match.group(3)),
2456 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002457 hostname=parsed_url.netloc,
2458 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002459 return None
2460
tandrii16e0b4e2016-06-07 10:34:28 -07002461 def _GerritCommitMsgHookCheck(self, offer_removal):
2462 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2463 if not os.path.exists(hook):
2464 return
2465 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2466 # custom developer made one.
2467 data = gclient_utils.FileRead(hook)
2468 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2469 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002470 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002471 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002472 'and may interfere with it in subtle ways.\n'
2473 'We recommend you remove the commit-msg hook.')
2474 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002475 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002476 gclient_utils.rm_file_or_tree(hook)
2477 print('Gerrit commit-msg hook removed.')
2478 else:
2479 print('OK, will keep Gerrit commit-msg hook in place.')
2480
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002481 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002482 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002483 if options.squash and options.no_squash:
2484 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002485
2486 if not options.squash and not options.no_squash:
2487 # Load default for user, repo, squash=true, in this order.
2488 options.squash = settings.GetSquashGerritUploads()
2489 elif options.no_squash:
2490 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002491
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002492 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002493 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002494 # This may be None; default fallback value is determined in logic below.
2495 title = options.title
2496
Dominic Battre7d1c4842017-10-27 09:17:28 +02002497 # Extract bug number from branch name.
2498 bug = options.bug
2499 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2500 if not bug and match:
2501 bug = match.group(1)
2502
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002503 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002504 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002505 if self.GetIssue():
2506 # Try to get the message from a previous upload.
2507 message = self.GetDescription()
2508 if not message:
2509 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002510 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002511 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002512 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002513 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002514 # When uploading a subsequent patchset, -m|--message is taken
2515 # as the patchset title if --title was not provided.
2516 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002517 else:
2518 default_title = RunGit(
2519 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002520 if options.force:
2521 title = default_title
2522 else:
2523 title = ask_for_data(
2524 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002525 change_id = self._GetChangeDetail()['change_id']
2526 while True:
2527 footer_change_ids = git_footers.get_footer_change_id(message)
2528 if footer_change_ids == [change_id]:
2529 break
2530 if not footer_change_ids:
2531 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002532 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002533 continue
2534 # There is already a valid footer but with different or several ids.
2535 # Doing this automatically is non-trivial as we don't want to lose
2536 # existing other footers, yet we want to append just 1 desired
2537 # Change-Id. Thus, just create a new footer, but let user verify the
2538 # new description.
2539 message = '%s\n\nChange-Id: %s' % (message, change_id)
2540 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002541 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002542 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002543 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002544 'Please, check the proposed correction to the description, '
2545 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2546 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2547 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002548 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002549 if not options.force:
2550 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002551 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002552 message = change_desc.description
2553 if not message:
2554 DieWithError("Description is empty. Aborting...")
2555 # Continue the while loop.
2556 # Sanity check of this code - we should end up with proper message
2557 # footer.
2558 assert [change_id] == git_footers.get_footer_change_id(message)
2559 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002560 else: # if not self.GetIssue()
2561 if options.message:
2562 message = options.message
2563 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002564 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002565 if options.title:
2566 message = options.title + '\n\n' + message
2567 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002568
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002569 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002570 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002571 # On first upload, patchset title is always this string, while
2572 # --title flag gets converted to first line of message.
2573 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002574 if not change_desc.description:
2575 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002576 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002577 if len(change_ids) > 1:
2578 DieWithError('too many Change-Id footers, at most 1 allowed.')
2579 if not change_ids:
2580 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002581 change_desc.set_description(git_footers.add_footer_change_id(
2582 change_desc.description,
2583 GenerateGerritChangeId(change_desc.description)))
2584 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002585 assert len(change_ids) == 1
2586 change_id = change_ids[0]
2587
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002588 if options.reviewers or options.tbrs or options.add_owners_to:
2589 change_desc.update_reviewers(options.reviewers, options.tbrs,
2590 options.add_owners_to, change)
2591
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002592 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002593 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2594 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002595 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002596 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2597 desc_tempfile.write(change_desc.description)
2598 desc_tempfile.close()
2599 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2600 '-F', desc_tempfile.name]).strip()
2601 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002602 else:
2603 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002604 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002605 if not change_desc.description:
2606 DieWithError("Description is empty. Aborting...")
2607
2608 if not git_footers.get_footer_change_id(change_desc.description):
2609 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002610 change_desc.set_description(
2611 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002612 if options.reviewers or options.tbrs or options.add_owners_to:
2613 change_desc.update_reviewers(options.reviewers, options.tbrs,
2614 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002615 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002616 # For no-squash mode, we assume the remote called "origin" is the one we
2617 # want. It is not worthwhile to support different workflows for
2618 # no-squash mode.
2619 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002620 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2621
2622 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002623 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002624 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2625 ref_to_push)]).splitlines()
2626 if len(commits) > 1:
2627 print('WARNING: This will upload %d commits. Run the following command '
2628 'to see which commits will be uploaded: ' % len(commits))
2629 print('git log %s..%s' % (parent, ref_to_push))
2630 print('You can also use `git squash-branch` to squash these into a '
2631 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002632 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002633
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002634 if options.reviewers or options.tbrs or options.add_owners_to:
2635 change_desc.update_reviewers(options.reviewers, options.tbrs,
2636 options.add_owners_to, change)
2637
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002638 reviewers = sorted(change_desc.get_reviewers())
2639 # Add cc's from the CC_LIST and --cc flag (if any).
2640 if not options.private and not options.no_autocc:
2641 cc = self.GetCCList().split(',')
2642 else:
2643 cc = []
2644 if options.cc:
2645 cc.extend(options.cc)
2646 cc = filter(None, [email.strip() for email in cc])
2647 if change_desc.get_cced():
2648 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002649 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2650 valid_accounts = set(reviewers + cc)
2651 # TODO(crbug/877717): relax this for all hosts.
2652 else:
2653 valid_accounts = gerrit_util.ValidAccounts(
2654 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002655 logging.info('accounts %s are recognized, %s invalid',
2656 sorted(valid_accounts),
2657 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002658
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002659 # Extra options that can be specified at push time. Doc:
2660 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002661 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002662
Aaron Gable844cf292017-06-28 11:32:59 -07002663 # By default, new changes are started in WIP mode, and subsequent patchsets
2664 # don't send email. At any time, passing --send-mail will mark the change
2665 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002666 if options.send_mail:
2667 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002668 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002669 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002670 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002671 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002672 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002673
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002674 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002675 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002676
Aaron Gable9b713dd2016-12-14 16:04:21 -08002677 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002678 # Punctuation and whitespace in |title| must be percent-encoded.
2679 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002680
agablec6787972016-09-09 16:13:34 -07002681 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002682 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002683
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002684 for r in sorted(reviewers):
2685 if r in valid_accounts:
2686 refspec_opts.append('r=%s' % r)
2687 reviewers.remove(r)
2688 else:
2689 # TODO(tandrii): this should probably be a hard failure.
2690 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2691 % r)
2692 for c in sorted(cc):
2693 # refspec option will be rejected if cc doesn't correspond to an
2694 # account, even though REST call to add such arbitrary cc may succeed.
2695 if c in valid_accounts:
2696 refspec_opts.append('cc=%s' % c)
2697 cc.remove(c)
2698
rmistry9eadede2016-09-19 11:22:43 -07002699 if options.topic:
2700 # Documentation on Gerrit topics is here:
2701 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002702 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002703
Edward Lemur687ca902018-12-05 02:30:30 +00002704 if options.enable_auto_submit:
2705 refspec_opts.append('l=Auto-Submit+1')
2706 if options.use_commit_queue:
2707 refspec_opts.append('l=Commit-Queue+2')
2708 elif options.cq_dry_run:
2709 refspec_opts.append('l=Commit-Queue+1')
2710
2711 if change_desc.get_reviewers(tbr_only=True):
2712 score = gerrit_util.GetCodeReviewTbrScore(
2713 self._GetGerritHost(),
2714 self._GetGerritProject())
2715 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002716
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002717 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002718 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002719 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002720 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002721 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2722
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002723 refspec_suffix = ''
2724 if refspec_opts:
2725 refspec_suffix = '%' + ','.join(refspec_opts)
2726 assert ' ' not in refspec_suffix, (
2727 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2728 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2729
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002730 try:
Raul Tambreba35c7f2019-03-24 06:11:18 +00002731 push_returncode = 0
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002732 before_push = time_time()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002733 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00002734 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemuredcefdc2018-11-08 14:41:42 +00002735 print_stdout=True,
Edward Lemur49c8eaf2018-11-07 22:13:12 +00002736 # Flush after every line: useful for seeing progress when running as
2737 # recipe.
2738 filter_fn=lambda _: sys.stdout.flush())
Edward Lemurfec80c42018-11-01 23:14:14 +00002739 except subprocess2.CalledProcessError as e:
2740 push_returncode = e.returncode
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002741 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002742 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002743 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002744 'credential problems:\n'
2745 ' git cl creds-check\n',
2746 change_desc)
Edward Lemurfec80c42018-11-01 23:14:14 +00002747 finally:
2748 metrics.collector.add_repeated('sub_commands', {
2749 'command': 'git push',
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002750 'execution_time': time_time() - before_push,
Edward Lemurfec80c42018-11-01 23:14:14 +00002751 'exit_code': push_returncode,
2752 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2753 })
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002754
2755 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002756 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002757 change_numbers = [m.group(1)
2758 for m in map(regex.match, push_stdout.splitlines())
2759 if m]
2760 if len(change_numbers) != 1:
2761 DieWithError(
2762 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002763 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002764 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002765 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002766
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002767 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002768 # GetIssue() is not set in case of non-squash uploads according to tests.
2769 # TODO(agable): non-squash uploads in git cl should be removed.
2770 gerrit_util.AddReviewers(
2771 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002772 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002773 reviewers, cc,
2774 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002775
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002776 return 0
2777
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002778 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2779 change_desc):
2780 """Computes parent of the generated commit to be uploaded to Gerrit.
2781
2782 Returns revision or a ref name.
2783 """
2784 if custom_cl_base:
2785 # Try to avoid creating additional unintended CLs when uploading, unless
2786 # user wants to take this risk.
2787 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2788 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2789 local_ref_of_target_remote])
2790 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002791 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002792 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2793 'If you proceed with upload, more than 1 CL may be created by '
2794 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2795 'If you are certain that specified base `%s` has already been '
2796 'uploaded to Gerrit as another CL, you may proceed.\n' %
2797 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2798 if not force:
2799 confirm_or_exit(
2800 'Do you take responsibility for cleaning up potential mess '
2801 'resulting from proceeding with upload?',
2802 action='upload')
2803 return custom_cl_base
2804
Aaron Gablef97e33d2017-03-30 15:44:27 -07002805 if remote != '.':
2806 return self.GetCommonAncestorWithUpstream()
2807
2808 # If our upstream branch is local, we base our squashed commit on its
2809 # squashed version.
2810 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2811
Aaron Gablef97e33d2017-03-30 15:44:27 -07002812 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002813 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002814
2815 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002816 # TODO(tandrii): consider checking parent change in Gerrit and using its
2817 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2818 # the tree hash of the parent branch. The upside is less likely bogus
2819 # requests to reupload parent change just because it's uploadhash is
2820 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002821 parent = RunGit(['config',
2822 'branch.%s.gerritsquashhash' % upstream_branch_name],
2823 error_ok=True).strip()
2824 # Verify that the upstream branch has been uploaded too, otherwise
2825 # Gerrit will create additional CLs when uploading.
2826 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2827 RunGitSilent(['rev-parse', parent + ':'])):
2828 DieWithError(
2829 '\nUpload upstream branch %s first.\n'
2830 'It is likely that this branch has been rebased since its last '
2831 'upload, so you just need to upload it again.\n'
2832 '(If you uploaded it with --no-squash, then branch dependencies '
2833 'are not supported, and you should reupload with --squash.)'
2834 % upstream_branch_name,
2835 change_desc)
2836 return parent
2837
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002838 def _AddChangeIdToCommitMessage(self, options, args):
2839 """Re-commits using the current message, assumes the commit hook is in
2840 place.
2841 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002842 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002843 git_command = ['commit', '--amend', '-m', log_desc]
2844 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002845 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002846 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002847 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002848 return new_log_desc
2849 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002850 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002851
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002852 def SetCQState(self, new_state):
2853 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002854 vote_map = {
2855 _CQState.NONE: 0,
2856 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002857 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002858 }
Aaron Gablefc62f762017-07-17 11:12:07 -07002859 labels = {'Commit-Queue': vote_map[new_state]}
2860 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002861 gerrit_util.SetReview(
2862 self._GetGerritHost(), self._GerritChangeIdentifier(),
2863 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002864
tandriie113dfd2016-10-11 10:20:12 -07002865 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002866 try:
2867 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002868 except GerritChangeNotExists:
2869 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002870
2871 if data['status'] in ('ABANDONED', 'MERGED'):
2872 return 'CL %s is closed' % self.GetIssue()
2873
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002874 def GetTryJobProperties(self, patchset=None):
2875 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002876 data = self._GetChangeDetail(['ALL_REVISIONS'])
2877 patchset = int(patchset or self.GetPatchset())
2878 assert patchset
2879 revision_data = None # Pylint wants it to be defined.
2880 for revision_data in data['revisions'].itervalues():
2881 if int(revision_data['_number']) == patchset:
2882 break
2883 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002884 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002885 (patchset, self.GetIssue()))
2886 return {
2887 'patch_issue': self.GetIssue(),
2888 'patch_set': patchset or self.GetPatchset(),
2889 'patch_project': data['project'],
2890 'patch_storage': 'gerrit',
2891 'patch_ref': revision_data['fetch']['http']['ref'],
2892 'patch_repository_url': revision_data['fetch']['http']['url'],
2893 'patch_gerrit_url': self.GetCodereviewServer(),
2894 }
tandriie113dfd2016-10-11 10:20:12 -07002895
tandriide281ae2016-10-12 06:02:30 -07002896 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002897 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002898
Edward Lemur707d70b2018-02-07 00:50:14 +01002899 def GetReviewers(self):
2900 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002901 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002902
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002903
2904_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002905 'gerrit': _GerritChangelistImpl,
2906}
2907
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002908
iannuccie53c9352016-08-17 14:40:40 -07002909def _add_codereview_issue_select_options(parser, extra=""):
2910 _add_codereview_select_options(parser)
2911
2912 text = ('Operate on this issue number instead of the current branch\'s '
2913 'implicit issue.')
2914 if extra:
2915 text += ' '+extra
2916 parser.add_option('-i', '--issue', type=int, help=text)
2917
2918
2919def _process_codereview_issue_select_options(parser, options):
2920 _process_codereview_select_options(parser, options)
2921 if options.issue is not None and not options.forced_codereview:
2922 parser.error('--issue must be specified with either --rietveld or --gerrit')
2923
2924
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002925def _add_codereview_select_options(parser):
2926 """Appends --gerrit and --rietveld options to force specific codereview."""
2927 parser.codereview_group = optparse.OptionGroup(
2928 parser, 'EXPERIMENTAL! Codereview override options')
2929 parser.add_option_group(parser.codereview_group)
2930 parser.codereview_group.add_option(
2931 '--gerrit', action='store_true',
2932 help='Force the use of Gerrit for codereview')
2933 parser.codereview_group.add_option(
2934 '--rietveld', action='store_true',
2935 help='Force the use of Rietveld for codereview')
2936
2937
2938def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00002939 if options.rietveld:
2940 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002941 options.forced_codereview = None
2942 if options.gerrit:
2943 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002944
2945
tandriif9aefb72016-07-01 09:06:51 -07002946def _get_bug_line_values(default_project, bugs):
2947 """Given default_project and comma separated list of bugs, yields bug line
2948 values.
2949
2950 Each bug can be either:
2951 * a number, which is combined with default_project
2952 * string, which is left as is.
2953
2954 This function may produce more than one line, because bugdroid expects one
2955 project per line.
2956
2957 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2958 ['v8:123', 'chromium:789']
2959 """
2960 default_bugs = []
2961 others = []
2962 for bug in bugs.split(','):
2963 bug = bug.strip()
2964 if bug:
2965 try:
2966 default_bugs.append(int(bug))
2967 except ValueError:
2968 others.append(bug)
2969
2970 if default_bugs:
2971 default_bugs = ','.join(map(str, default_bugs))
2972 if default_project:
2973 yield '%s:%s' % (default_project, default_bugs)
2974 else:
2975 yield default_bugs
2976 for other in sorted(others):
2977 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2978 yield other
2979
2980
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002981class ChangeDescription(object):
2982 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002983 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002984 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002985 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002986 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002987 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2988 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
2989 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
2990 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002991
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002992 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002993 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002994
agable@chromium.org42c20792013-09-12 17:34:49 +00002995 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002996 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002997 return '\n'.join(self._description_lines)
2998
2999 def set_description(self, desc):
3000 if isinstance(desc, basestring):
3001 lines = desc.splitlines()
3002 else:
3003 lines = [line.rstrip() for line in desc]
3004 while lines and not lines[0]:
3005 lines.pop(0)
3006 while lines and not lines[-1]:
3007 lines.pop(-1)
3008 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003009
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003010 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3011 """Rewrites the R=/TBR= line(s) as a single line each.
3012
3013 Args:
3014 reviewers (list(str)) - list of additional emails to use for reviewers.
3015 tbrs (list(str)) - list of additional emails to use for TBRs.
3016 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3017 the change that are missing OWNER coverage. If this is not None, you
3018 must also pass a value for `change`.
3019 change (Change) - The Change that should be used for OWNERS lookups.
3020 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003021 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003022 assert isinstance(tbrs, list), tbrs
3023
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003024 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003025 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003026
3027 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003028 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003029
3030 reviewers = set(reviewers)
3031 tbrs = set(tbrs)
3032 LOOKUP = {
3033 'TBR': tbrs,
3034 'R': reviewers,
3035 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003036
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003037 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003038 regexp = re.compile(self.R_LINE)
3039 matches = [regexp.match(line) for line in self._description_lines]
3040 new_desc = [l for i, l in enumerate(self._description_lines)
3041 if not matches[i]]
3042 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003043
agable@chromium.org42c20792013-09-12 17:34:49 +00003044 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003045
3046 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003047 for match in matches:
3048 if not match:
3049 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003050 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3051
3052 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003053 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003054 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003055 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003056 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003057 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003058 LOOKUP[add_owners_to].update(
3059 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003060
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003061 # If any folks ended up in both groups, remove them from tbrs.
3062 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003063
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003064 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3065 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003066
3067 # Put the new lines in the description where the old first R= line was.
3068 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3069 if 0 <= line_loc < len(self._description_lines):
3070 if new_tbr_line:
3071 self._description_lines.insert(line_loc, new_tbr_line)
3072 if new_r_line:
3073 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003074 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003075 if new_r_line:
3076 self.append_footer(new_r_line)
3077 if new_tbr_line:
3078 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003079
Aaron Gable3a16ed12017-03-23 10:51:55 -07003080 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003081 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003082 self.set_description([
3083 '# Enter a description of the change.',
3084 '# This will be displayed on the codereview site.',
3085 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003086 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003087 '--------------------',
3088 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003089
agable@chromium.org42c20792013-09-12 17:34:49 +00003090 regexp = re.compile(self.BUG_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003091 prefix = settings.GetBugPrefix()
agable@chromium.org42c20792013-09-12 17:34:49 +00003092 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003093 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003094 if git_footer:
3095 self.append_footer('Bug: %s' % ', '.join(values))
3096 else:
3097 for value in values:
3098 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003099
agable@chromium.org42c20792013-09-12 17:34:49 +00003100 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003101 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003102 if not content:
3103 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003104 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003105
Bruce Dawson2377b012018-01-11 16:46:49 -08003106 # Strip off comments and default inserted "Bug:" line.
3107 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003108 (line.startswith('#') or
3109 line.rstrip() == "Bug:" or
3110 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003111 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003112 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003113 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003114
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003115 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003116 """Adds a footer line to the description.
3117
3118 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3119 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3120 that Gerrit footers are always at the end.
3121 """
3122 parsed_footer_line = git_footers.parse_footer(line)
3123 if parsed_footer_line:
3124 # Line is a gerrit footer in the form: Footer-Key: any value.
3125 # Thus, must be appended observing Gerrit footer rules.
3126 self.set_description(
3127 git_footers.add_footer(self.description,
3128 key=parsed_footer_line[0],
3129 value=parsed_footer_line[1]))
3130 return
3131
3132 if not self._description_lines:
3133 self._description_lines.append(line)
3134 return
3135
3136 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3137 if gerrit_footers:
3138 # git_footers.split_footers ensures that there is an empty line before
3139 # actual (gerrit) footers, if any. We have to keep it that way.
3140 assert top_lines and top_lines[-1] == ''
3141 top_lines, separator = top_lines[:-1], top_lines[-1:]
3142 else:
3143 separator = [] # No need for separator if there are no gerrit_footers.
3144
3145 prev_line = top_lines[-1] if top_lines else ''
3146 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3147 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3148 top_lines.append('')
3149 top_lines.append(line)
3150 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003151
tandrii99a72f22016-08-17 14:33:24 -07003152 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003153 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003154 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003155 reviewers = [match.group(2).strip()
3156 for match in matches
3157 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003158 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003159
bradnelsond975b302016-10-23 12:20:23 -07003160 def get_cced(self):
3161 """Retrieves the list of reviewers."""
3162 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3163 cced = [match.group(2).strip() for match in matches if match]
3164 return cleanup_list(cced)
3165
Nodir Turakulov23b82142017-11-16 11:04:25 -08003166 def get_hash_tags(self):
3167 """Extracts and sanitizes a list of Gerrit hashtags."""
3168 subject = (self._description_lines or ('',))[0]
3169 subject = re.sub(
3170 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3171
3172 tags = []
3173 start = 0
3174 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3175 while True:
3176 m = bracket_exp.match(subject, start)
3177 if not m:
3178 break
3179 tags.append(self.sanitize_hash_tag(m.group(1)))
3180 start = m.end()
3181
3182 if not tags:
3183 # Try "Tag: " prefix.
3184 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3185 if m:
3186 tags.append(self.sanitize_hash_tag(m.group(1)))
3187 return tags
3188
3189 @classmethod
3190 def sanitize_hash_tag(cls, tag):
3191 """Returns a sanitized Gerrit hash tag.
3192
3193 A sanitized hashtag can be used as a git push refspec parameter value.
3194 """
3195 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3196
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003197 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3198 """Updates this commit description given the parent.
3199
3200 This is essentially what Gnumbd used to do.
3201 Consult https://goo.gl/WMmpDe for more details.
3202 """
3203 assert parent_msg # No, orphan branch creation isn't supported.
3204 assert parent_hash
3205 assert dest_ref
3206 parent_footer_map = git_footers.parse_footers(parent_msg)
3207 # This will also happily parse svn-position, which GnumbD is no longer
3208 # supporting. While we'd generate correct footers, the verifier plugin
3209 # installed in Gerrit will block such commit (ie git push below will fail).
3210 parent_position = git_footers.get_position(parent_footer_map)
3211
3212 # Cherry-picks may have last line obscuring their prior footers,
3213 # from git_footers perspective. This is also what Gnumbd did.
3214 cp_line = None
3215 if (self._description_lines and
3216 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3217 cp_line = self._description_lines.pop()
3218
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003219 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003220
3221 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3222 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003223 for i, line in enumerate(footer_lines):
3224 k, v = git_footers.parse_footer(line) or (None, None)
3225 if k and k.startswith('Cr-'):
3226 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003227
3228 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003229 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003230 if parent_position[0] == dest_ref:
3231 # Same branch as parent.
3232 number = int(parent_position[1]) + 1
3233 else:
3234 number = 1 # New branch, and extra lineage.
3235 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3236 int(parent_position[1])))
3237
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003238 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3239 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003240
3241 self._description_lines = top_lines
3242 if cp_line:
3243 self._description_lines.append(cp_line)
3244 if self._description_lines[-1] != '':
3245 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003246 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003247
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003248
Aaron Gablea1bab272017-04-11 16:38:18 -07003249def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003250 """Retrieves the reviewers that approved a CL from the issue properties with
3251 messages.
3252
3253 Note that the list may contain reviewers that are not committer, thus are not
3254 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003255
3256 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003257 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003258 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003259 return sorted(
3260 set(
3261 message['sender']
3262 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003263 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003264 )
3265 )
3266
3267
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003268def FindCodereviewSettingsFile(filename='codereview.settings'):
3269 """Finds the given file starting in the cwd and going up.
3270
3271 Only looks up to the top of the repository unless an
3272 'inherit-review-settings-ok' file exists in the root of the repository.
3273 """
3274 inherit_ok_file = 'inherit-review-settings-ok'
3275 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003276 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003277 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3278 root = '/'
3279 while True:
3280 if filename in os.listdir(cwd):
3281 if os.path.isfile(os.path.join(cwd, filename)):
3282 return open(os.path.join(cwd, filename))
3283 if cwd == root:
3284 break
3285 cwd = os.path.dirname(cwd)
3286
3287
3288def LoadCodereviewSettingsFromFile(fileobj):
3289 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003290 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003291
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003292 def SetProperty(name, setting, unset_error_ok=False):
3293 fullname = 'rietveld.' + name
3294 if setting in keyvals:
3295 RunGit(['config', fullname, keyvals[setting]])
3296 else:
3297 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3298
tandrii48df5812016-10-17 03:55:37 -07003299 if not keyvals.get('GERRIT_HOST', False):
3300 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003301 # Only server setting is required. Other settings can be absent.
3302 # In that case, we ignore errors raised during option deletion attempt.
3303 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3304 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3305 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003306 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003307 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3308 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003309 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3310 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003311
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003312 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003313 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003314
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003315 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003316 RunGit(['config', 'gerrit.squash-uploads',
3317 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003318
tandrii@chromium.org28253532016-04-14 13:46:56 +00003319 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003320 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003321 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3322
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003323 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003324 # should be of the form
3325 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3326 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003327 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3328 keyvals['ORIGIN_URL_CONFIG']])
3329
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003330
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003331def urlretrieve(source, destination):
3332 """urllib is broken for SSL connections via a proxy therefore we
3333 can't use urllib.urlretrieve()."""
3334 with open(destination, 'w') as f:
3335 f.write(urllib2.urlopen(source).read())
3336
3337
ukai@chromium.org712d6102013-11-27 00:52:58 +00003338def hasSheBang(fname):
3339 """Checks fname is a #! script."""
3340 with open(fname) as f:
3341 return f.read(2).startswith('#!')
3342
3343
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003344# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3345def DownloadHooks(*args, **kwargs):
3346 pass
3347
3348
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003349def DownloadGerritHook(force):
3350 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003351
3352 Args:
3353 force: True to update hooks. False to install hooks if not present.
3354 """
3355 if not settings.GetIsGerrit():
3356 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003357 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003358 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3359 if not os.access(dst, os.X_OK):
3360 if os.path.exists(dst):
3361 if not force:
3362 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003363 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003364 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003365 if not hasSheBang(dst):
3366 DieWithError('Not a script: %s\n'
3367 'You need to download from\n%s\n'
3368 'into .git/hooks/commit-msg and '
3369 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003370 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3371 except Exception:
3372 if os.path.exists(dst):
3373 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003374 DieWithError('\nFailed to download hooks.\n'
3375 'You need to download from\n%s\n'
3376 'into .git/hooks/commit-msg and '
3377 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003378
3379
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003380class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003381 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003382
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003383 _GOOGLESOURCE = 'googlesource.com'
3384
3385 def __init__(self):
3386 # Cached list of [host, identity, source], where source is either
3387 # .gitcookies or .netrc.
3388 self._all_hosts = None
3389
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003390 def ensure_configured_gitcookies(self):
3391 """Runs checks and suggests fixes to make git use .gitcookies from default
3392 path."""
3393 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3394 configured_path = RunGitSilent(
3395 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003396 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003397 if configured_path:
3398 self._ensure_default_gitcookies_path(configured_path, default)
3399 else:
3400 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003401
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003402 @staticmethod
3403 def _ensure_default_gitcookies_path(configured_path, default_path):
3404 assert configured_path
3405 if configured_path == default_path:
3406 print('git is already configured to use your .gitcookies from %s' %
3407 configured_path)
3408 return
3409
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003410 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003411 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3412 (configured_path, default_path))
3413
3414 if not os.path.exists(configured_path):
3415 print('However, your configured .gitcookies file is missing.')
3416 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3417 action='reconfigure')
3418 RunGit(['config', '--global', 'http.cookiefile', default_path])
3419 return
3420
3421 if os.path.exists(default_path):
3422 print('WARNING: default .gitcookies file already exists %s' %
3423 default_path)
3424 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3425 default_path)
3426
3427 confirm_or_exit('Move existing .gitcookies to default location?',
3428 action='move')
3429 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003430 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003431 print('Moved and reconfigured git to use .gitcookies from %s' %
3432 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003433
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003434 @staticmethod
3435 def _configure_gitcookies_path(default_path):
3436 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3437 if os.path.exists(netrc_path):
3438 print('You seem to be using outdated .netrc for git credentials: %s' %
3439 netrc_path)
3440 print('This tool will guide you through setting up recommended '
3441 '.gitcookies store for git credentials.\n'
3442 '\n'
3443 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3444 ' git config --global --unset http.cookiefile\n'
3445 ' mv %s %s.backup\n\n' % (default_path, default_path))
3446 confirm_or_exit(action='setup .gitcookies')
3447 RunGit(['config', '--global', 'http.cookiefile', default_path])
3448 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003449
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003450 def get_hosts_with_creds(self, include_netrc=False):
3451 if self._all_hosts is None:
3452 a = gerrit_util.CookiesAuthenticator()
3453 self._all_hosts = [
3454 (h, u, s)
3455 for h, u, s in itertools.chain(
3456 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3457 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3458 )
3459 if h.endswith(self._GOOGLESOURCE)
3460 ]
3461
3462 if include_netrc:
3463 return self._all_hosts
3464 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3465
3466 def print_current_creds(self, include_netrc=False):
3467 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3468 if not hosts:
3469 print('No Git/Gerrit credentials found')
3470 return
3471 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3472 header = [('Host', 'User', 'Which file'),
3473 ['=' * l for l in lengths]]
3474 for row in (header + hosts):
3475 print('\t'.join((('%%+%ds' % l) % s)
3476 for l, s in zip(lengths, row)))
3477
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003478 @staticmethod
3479 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003480 """Parses identity "git-<username>.domain" into <username> and domain."""
3481 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003482 # distinguishable from sub-domains. But we do know typical domains:
3483 if identity.endswith('.chromium.org'):
3484 domain = 'chromium.org'
3485 username = identity[:-len('.chromium.org')]
3486 else:
3487 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003488 if username.startswith('git-'):
3489 username = username[len('git-'):]
3490 return username, domain
3491
3492 def _get_usernames_of_domain(self, domain):
3493 """Returns list of usernames referenced by .gitcookies in a given domain."""
3494 identities_by_domain = {}
3495 for _, identity, _ in self.get_hosts_with_creds():
3496 username, domain = self._parse_identity(identity)
3497 identities_by_domain.setdefault(domain, []).append(username)
3498 return identities_by_domain.get(domain)
3499
3500 def _canonical_git_googlesource_host(self, host):
3501 """Normalizes Gerrit hosts (with '-review') to Git host."""
3502 assert host.endswith(self._GOOGLESOURCE)
3503 # Prefix doesn't include '.' at the end.
3504 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3505 if prefix.endswith('-review'):
3506 prefix = prefix[:-len('-review')]
3507 return prefix + '.' + self._GOOGLESOURCE
3508
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003509 def _canonical_gerrit_googlesource_host(self, host):
3510 git_host = self._canonical_git_googlesource_host(host)
3511 prefix = git_host.split('.', 1)[0]
3512 return prefix + '-review.' + self._GOOGLESOURCE
3513
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003514 def _get_counterpart_host(self, host):
3515 assert host.endswith(self._GOOGLESOURCE)
3516 git = self._canonical_git_googlesource_host(host)
3517 gerrit = self._canonical_gerrit_googlesource_host(git)
3518 return git if gerrit == host else gerrit
3519
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003520 def has_generic_host(self):
3521 """Returns whether generic .googlesource.com has been configured.
3522
3523 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3524 """
3525 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3526 if host == '.' + self._GOOGLESOURCE:
3527 return True
3528 return False
3529
3530 def _get_git_gerrit_identity_pairs(self):
3531 """Returns map from canonic host to pair of identities (Git, Gerrit).
3532
3533 One of identities might be None, meaning not configured.
3534 """
3535 host_to_identity_pairs = {}
3536 for host, identity, _ in self.get_hosts_with_creds():
3537 canonical = self._canonical_git_googlesource_host(host)
3538 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3539 idx = 0 if canonical == host else 1
3540 pair[idx] = identity
3541 return host_to_identity_pairs
3542
3543 def get_partially_configured_hosts(self):
3544 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003545 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3546 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3547 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003548
3549 def get_conflicting_hosts(self):
3550 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003551 host
3552 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003553 if None not in (i1, i2) and i1 != i2)
3554
3555 def get_duplicated_hosts(self):
3556 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3557 return set(host for host, count in counters.iteritems() if count > 1)
3558
3559 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3560 'chromium.googlesource.com': 'chromium.org',
3561 'chrome-internal.googlesource.com': 'google.com',
3562 }
3563
3564 def get_hosts_with_wrong_identities(self):
3565 """Finds hosts which **likely** reference wrong identities.
3566
3567 Note: skips hosts which have conflicting identities for Git and Gerrit.
3568 """
3569 hosts = set()
3570 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3571 pair = self._get_git_gerrit_identity_pairs().get(host)
3572 if pair and pair[0] == pair[1]:
3573 _, domain = self._parse_identity(pair[0])
3574 if domain != expected:
3575 hosts.add(host)
3576 return hosts
3577
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003578 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003579 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003580 hosts = sorted(hosts)
3581 assert hosts
3582 if extra_column_func is None:
3583 extras = [''] * len(hosts)
3584 else:
3585 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003586 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3587 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003588 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003589 lines.append(tmpl % he)
3590 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003591
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003592 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003593 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003594 yield ('.googlesource.com wildcard record detected',
3595 ['Chrome Infrastructure team recommends to list full host names '
3596 'explicitly.'],
3597 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003598
3599 dups = self.get_duplicated_hosts()
3600 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003601 yield ('The following hosts were defined twice',
3602 self._format_hosts(dups),
3603 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003604
3605 partial = self.get_partially_configured_hosts()
3606 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003607 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3608 'These hosts are missing',
3609 self._format_hosts(partial, lambda host: 'but %s defined' %
3610 self._get_counterpart_host(host)),
3611 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003612
3613 conflicting = self.get_conflicting_hosts()
3614 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003615 yield ('The following Git hosts have differing credentials from their '
3616 'Gerrit counterparts',
3617 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3618 tuple(self._get_git_gerrit_identity_pairs()[host])),
3619 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003620
3621 wrong = self.get_hosts_with_wrong_identities()
3622 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003623 yield ('These hosts likely use wrong identity',
3624 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3625 (self._get_git_gerrit_identity_pairs()[host][0],
3626 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3627 wrong)
3628
3629 def find_and_report_problems(self):
3630 """Returns True if there was at least one problem, else False."""
3631 found = False
3632 bad_hosts = set()
3633 for title, sublines, hosts in self._find_problems():
3634 if not found:
3635 found = True
3636 print('\n\n.gitcookies problem report:\n')
3637 bad_hosts.update(hosts or [])
3638 print(' %s%s' % (title , (':' if sublines else '')))
3639 if sublines:
3640 print()
3641 print(' %s' % '\n '.join(sublines))
3642 print()
3643
3644 if bad_hosts:
3645 assert found
3646 print(' You can manually remove corresponding lines in your %s file and '
3647 'visit the following URLs with correct account to generate '
3648 'correct credential lines:\n' %
3649 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3650 print(' %s' % '\n '.join(sorted(set(
3651 gerrit_util.CookiesAuthenticator().get_new_password_url(
3652 self._canonical_git_googlesource_host(host))
3653 for host in bad_hosts
3654 ))))
3655 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003656
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003657
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003658@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003659def CMDcreds_check(parser, args):
3660 """Checks credentials and suggests changes."""
3661 _, _ = parser.parse_args(args)
3662
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003663 # Code below checks .gitcookies. Abort if using something else.
3664 authn = gerrit_util.Authenticator.get()
3665 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3666 if isinstance(authn, gerrit_util.GceAuthenticator):
3667 DieWithError(
3668 'This command is not designed for GCE, are you on a bot?\n'
3669 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3670 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003671 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003672 'This command is not designed for bot environment. It checks '
3673 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003674
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003675 checker = _GitCookiesChecker()
3676 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003677
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003678 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003679 checker.print_current_creds(include_netrc=True)
3680
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003681 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003682 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003683 return 0
3684 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003685
3686
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003687@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003688def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003689 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003690 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3691 branch = ShortBranchName(branchref)
3692 _, args = parser.parse_args(args)
3693 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003694 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003695 return RunGit(['config', 'branch.%s.base-url' % branch],
3696 error_ok=False).strip()
3697 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003698 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003699 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3700 error_ok=False).strip()
3701
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003702def color_for_status(status):
3703 """Maps a Changelist status to color, for CMDstatus and other tools."""
3704 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003705 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003706 'waiting': Fore.BLUE,
3707 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003708 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003709 'lgtm': Fore.GREEN,
3710 'commit': Fore.MAGENTA,
3711 'closed': Fore.CYAN,
3712 'error': Fore.WHITE,
3713 }.get(status, Fore.WHITE)
3714
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003715
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003716def get_cl_statuses(changes, fine_grained, max_processes=None):
3717 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003718
3719 If fine_grained is true, this will fetch CL statuses from the server.
3720 Otherwise, simply indicate if there's a matching url for the given branches.
3721
3722 If max_processes is specified, it is used as the maximum number of processes
3723 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3724 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003725
3726 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003727 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003728 if not changes:
3729 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003730
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003731 if not fine_grained:
3732 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003733 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003734 for cl in changes:
3735 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003736 return
3737
3738 # First, sort out authentication issues.
3739 logging.debug('ensuring credentials exist')
3740 for cl in changes:
3741 cl.EnsureAuthenticated(force=False, refresh=True)
3742
3743 def fetch(cl):
3744 try:
3745 return (cl, cl.GetStatus())
3746 except:
3747 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003748 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003749 raise
3750
3751 threads_count = len(changes)
3752 if max_processes:
3753 threads_count = max(1, min(threads_count, max_processes))
3754 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3755
3756 pool = ThreadPool(threads_count)
3757 fetched_cls = set()
3758 try:
3759 it = pool.imap_unordered(fetch, changes).__iter__()
3760 while True:
3761 try:
3762 cl, status = it.next(timeout=5)
3763 except multiprocessing.TimeoutError:
3764 break
3765 fetched_cls.add(cl)
3766 yield cl, status
3767 finally:
3768 pool.close()
3769
3770 # Add any branches that failed to fetch.
3771 for cl in set(changes) - fetched_cls:
3772 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003773
rmistry@google.com2dd99862015-06-22 12:22:18 +00003774
3775def upload_branch_deps(cl, args):
3776 """Uploads CLs of local branches that are dependents of the current branch.
3777
3778 If the local branch dependency tree looks like:
3779 test1 -> test2.1 -> test3.1
3780 -> test3.2
3781 -> test2.2 -> test3.3
3782
3783 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3784 run on the dependent branches in this order:
3785 test2.1, test3.1, test3.2, test2.2, test3.3
3786
3787 Note: This function does not rebase your local dependent branches. Use it when
3788 you make a change to the parent branch that will not conflict with its
3789 dependent branches, and you would like their dependencies updated in
3790 Rietveld.
3791 """
3792 if git_common.is_dirty_git_tree('upload-branch-deps'):
3793 return 1
3794
3795 root_branch = cl.GetBranch()
3796 if root_branch is None:
3797 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3798 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003799 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003800 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3801 'patchset dependencies without an uploaded CL.')
3802
3803 branches = RunGit(['for-each-ref',
3804 '--format=%(refname:short) %(upstream:short)',
3805 'refs/heads'])
3806 if not branches:
3807 print('No local branches found.')
3808 return 0
3809
3810 # Create a dictionary of all local branches to the branches that are dependent
3811 # on it.
3812 tracked_to_dependents = collections.defaultdict(list)
3813 for b in branches.splitlines():
3814 tokens = b.split()
3815 if len(tokens) == 2:
3816 branch_name, tracked = tokens
3817 tracked_to_dependents[tracked].append(branch_name)
3818
vapiera7fbd5a2016-06-16 09:17:49 -07003819 print()
3820 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003821 dependents = []
3822 def traverse_dependents_preorder(branch, padding=''):
3823 dependents_to_process = tracked_to_dependents.get(branch, [])
3824 padding += ' '
3825 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003826 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003827 dependents.append(dependent)
3828 traverse_dependents_preorder(dependent, padding)
3829 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003830 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003831
3832 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003833 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003834 return 0
3835
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003836 confirm_or_exit('This command will checkout all dependent branches and run '
3837 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003838
rmistry@google.com2dd99862015-06-22 12:22:18 +00003839 # Record all dependents that failed to upload.
3840 failures = {}
3841 # Go through all dependents, checkout the branch and upload.
3842 try:
3843 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003844 print()
3845 print('--------------------------------------')
3846 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003847 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003848 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003849 try:
3850 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003851 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003852 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003853 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003854 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003855 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003856 finally:
3857 # Swap back to the original root branch.
3858 RunGit(['checkout', '-q', root_branch])
3859
vapiera7fbd5a2016-06-16 09:17:49 -07003860 print()
3861 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003862 for dependent_branch in dependents:
3863 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003864 print(' %s : %s' % (dependent_branch, upload_status))
3865 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003866
3867 return 0
3868
3869
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003870@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003871def CMDarchive(parser, args):
3872 """Archives and deletes branches associated with closed changelists."""
3873 parser.add_option(
3874 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003875 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003876 parser.add_option(
3877 '-f', '--force', action='store_true',
3878 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003879 parser.add_option(
3880 '-d', '--dry-run', action='store_true',
3881 help='Skip the branch tagging and removal steps.')
3882 parser.add_option(
3883 '-t', '--notags', action='store_true',
3884 help='Do not tag archived branches. '
3885 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003886
3887 auth.add_auth_options(parser)
3888 options, args = parser.parse_args(args)
3889 if args:
3890 parser.error('Unsupported args: %s' % ' '.join(args))
3891 auth_config = auth.extract_auth_config_from_options(options)
3892
3893 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3894 if not branches:
3895 return 0
3896
vapiera7fbd5a2016-06-16 09:17:49 -07003897 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003898 changes = [Changelist(branchref=b, auth_config=auth_config)
3899 for b in branches.splitlines()]
3900 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3901 statuses = get_cl_statuses(changes,
3902 fine_grained=True,
3903 max_processes=options.maxjobs)
3904 proposal = [(cl.GetBranch(),
3905 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3906 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003907 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003908 proposal.sort()
3909
3910 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003911 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003912 return 0
3913
3914 current_branch = GetCurrentBranch()
3915
vapiera7fbd5a2016-06-16 09:17:49 -07003916 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003917 if options.notags:
3918 for next_item in proposal:
3919 print(' ' + next_item[0])
3920 else:
3921 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3922 for next_item in proposal:
3923 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003924
kmarshall9249e012016-08-23 12:02:16 -07003925 # Quit now on precondition failure or if instructed by the user, either
3926 # via an interactive prompt or by command line flags.
3927 if options.dry_run:
3928 print('\nNo changes were made (dry run).\n')
3929 return 0
3930 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003931 print('You are currently on a branch \'%s\' which is associated with a '
3932 'closed codereview issue, so archive cannot proceed. Please '
3933 'checkout another branch and run this command again.' %
3934 current_branch)
3935 return 1
kmarshall9249e012016-08-23 12:02:16 -07003936 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003937 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3938 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003939 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003940 return 1
3941
3942 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003943 if not options.notags:
3944 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003945 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003946
vapiera7fbd5a2016-06-16 09:17:49 -07003947 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003948
3949 return 0
3950
3951
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003952@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003953def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003954 """Show status of changelists.
3955
3956 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003957 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003958 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003959 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003960 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003961 - Magenta in the commit queue
3962 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003963 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003964
3965 Also see 'git cl comments'.
3966 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00003967 parser.add_option(
3968 '--no-branch-color',
3969 action='store_true',
3970 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003971 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003972 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003973 parser.add_option('-f', '--fast', action='store_true',
3974 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003975 parser.add_option(
3976 '-j', '--maxjobs', action='store', type=int,
3977 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003978
3979 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003980 _add_codereview_issue_select_options(
3981 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003982 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003983 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003984 if args:
3985 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003986 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003987
iannuccie53c9352016-08-17 14:40:40 -07003988 if options.issue is not None and not options.field:
3989 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003991 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003992 cl = Changelist(auth_config=auth_config, issue=options.issue,
3993 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003994 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003995 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003996 elif options.field == 'id':
3997 issueid = cl.GetIssue()
3998 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003999 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004000 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004001 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004002 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004003 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004004 elif options.field == 'status':
4005 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004006 elif options.field == 'url':
4007 url = cl.GetIssueURL()
4008 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004009 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004010 return 0
4011
4012 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4013 if not branches:
4014 print('No local branch found.')
4015 return 0
4016
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004017 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004018 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004019 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004020 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004021 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004022 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004023 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004024
Daniel McArdlea23bf592019-02-12 00:25:12 +00004025 current_branch = GetCurrentBranch()
4026
4027 def FormatBranchName(branch, colorize=False):
4028 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
4029 an asterisk when it is the current branch."""
4030
4031 asterisk = ""
4032 color = Fore.RESET
4033 if branch == current_branch:
4034 asterisk = "* "
4035 color = Fore.GREEN
4036 branch_name = ShortBranchName(branch)
4037
4038 if colorize:
4039 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00004040 return asterisk + branch_name
4041
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004042 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004043
4044 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004045 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4046 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004047 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004048 c, status = output.next()
4049 branch_statuses[c.GetBranch()] = status
4050 status = branch_statuses.pop(branch)
4051 url = cl.GetIssueURL()
4052 if url and (not status or status == 'error'):
4053 # The issue probably doesn't exist anymore.
4054 url += ' (broken)'
4055
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004056 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004057 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004058 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004059 color = ''
4060 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004061 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004062
Alan Cuttera3be9a52019-03-04 18:50:33 +00004063 branch_display = FormatBranchName(branch)
4064 padding = ' ' * (alignment - len(branch_display))
4065 if not options.no_branch_color:
4066 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004067
Alan Cuttera3be9a52019-03-04 18:50:33 +00004068 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
4069 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004070
vapiera7fbd5a2016-06-16 09:17:49 -07004071 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004072 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004073 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004074 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004075 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004076 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004077 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004078 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004079 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004080 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004081 print('Issue description:')
4082 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004083 return 0
4084
4085
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004086def colorize_CMDstatus_doc():
4087 """To be called once in main() to add colors to git cl status help."""
4088 colors = [i for i in dir(Fore) if i[0].isupper()]
4089
4090 def colorize_line(line):
4091 for color in colors:
4092 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004093 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004094 indent = len(line) - len(line.lstrip(' ')) + 1
4095 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4096 return line
4097
4098 lines = CMDstatus.__doc__.splitlines()
4099 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4100
4101
phajdan.jre328cf92016-08-22 04:12:17 -07004102def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004103 if path == '-':
4104 json.dump(contents, sys.stdout)
4105 else:
4106 with open(path, 'w') as f:
4107 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004108
4109
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004110@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004111@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004112def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004113 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004114
4115 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004116 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004117 parser.add_option('-r', '--reverse', action='store_true',
4118 help='Lookup the branch(es) for the specified issues. If '
4119 'no issues are specified, all branches with mapped '
4120 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004121 parser.add_option('--json',
4122 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004123 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004124 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004125 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004126
dnj@chromium.org406c4402015-03-03 17:22:28 +00004127 if options.reverse:
4128 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004129 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004130 # Reverse issue lookup.
4131 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004132
4133 git_config = {}
4134 for config in RunGit(['config', '--get-regexp',
4135 r'branch\..*issue']).splitlines():
4136 name, _space, val = config.partition(' ')
4137 git_config[name] = val
4138
dnj@chromium.org406c4402015-03-03 17:22:28 +00004139 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004140 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4141 config_key = _git_branch_config_key(ShortBranchName(branch),
4142 cls.IssueConfigKey())
4143 issue = git_config.get(config_key)
4144 if issue:
4145 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004146 if not args:
4147 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004148 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004149 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004150 try:
4151 issue_num = int(issue)
4152 except ValueError:
4153 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004154 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004155 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004156 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004157 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004158 if options.json:
4159 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004160 return 0
4161
4162 if len(args) > 0:
4163 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4164 if not issue.valid:
4165 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4166 'or no argument to list it.\n'
4167 'Maybe you want to run git cl status?')
4168 cl = Changelist(codereview=issue.codereview)
4169 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004170 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004171 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004172 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4173 if options.json:
4174 write_json(options.json, {
4175 'issue': cl.GetIssue(),
4176 'issue_url': cl.GetIssueURL(),
4177 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004178 return 0
4179
4180
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004181@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004182def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004183 """Shows or posts review comments for any changelist."""
4184 parser.add_option('-a', '--add-comment', dest='comment',
4185 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004186 parser.add_option('-p', '--publish', action='store_true',
4187 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004188 parser.add_option('-i', '--issue', dest='issue',
4189 help='review issue id (defaults to current issue). '
4190 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004191 parser.add_option('-m', '--machine-readable', dest='readable',
4192 action='store_false', default=True,
4193 help='output comments in a format compatible with '
4194 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004195 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004196 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004197 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004198 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004199 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004200 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004201 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004202
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004203 issue = None
4204 if options.issue:
4205 try:
4206 issue = int(options.issue)
4207 except ValueError:
4208 DieWithError('A review issue id is expected to be a number')
4209
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004210 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4211
4212 if not cl.IsGerrit():
4213 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004214
4215 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004216 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004217 return 0
4218
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004219 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4220 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004221 for comment in summary:
4222 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004223 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004224 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004225 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004226 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004227 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004228 elif comment.autogenerated:
4229 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004230 else:
4231 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004232 print('\n%s%s %s%s\n%s' % (
4233 color,
4234 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4235 comment.sender,
4236 Fore.RESET,
4237 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4238
smut@google.comc85ac942015-09-15 16:34:43 +00004239 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004240 def pre_serialize(c):
4241 dct = c.__dict__.copy()
4242 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4243 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004244 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004245 return 0
4246
4247
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004248@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004249@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004250def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004251 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004252 parser.add_option('-d', '--display', action='store_true',
4253 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004254 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004255 help='New description to set for this issue (- for stdin, '
4256 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004257 parser.add_option('-f', '--force', action='store_true',
4258 help='Delete any unpublished Gerrit edits for this issue '
4259 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004260
4261 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004262 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004263 options, args = parser.parse_args(args)
4264 _process_codereview_select_options(parser, options)
4265
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004266 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004267 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004268 target_issue_arg = ParseIssueNumberArgument(args[0],
4269 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004270 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004271 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004272
martiniss6eda05f2016-06-30 10:18:35 -07004273 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004274 'auth_config': auth.extract_auth_config_from_options(options),
4275 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004276 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004277 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004278 if target_issue_arg:
4279 kwargs['issue'] = target_issue_arg.issue
4280 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004281 if target_issue_arg.codereview and not options.forced_codereview:
4282 detected_codereview_from_url = True
4283 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004284
4285 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004286 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004287 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004288 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004289
4290 if detected_codereview_from_url:
4291 logging.info('canonical issue/change URL: %s (type: %s)\n',
4292 cl.GetIssueURL(), target_issue_arg.codereview)
4293
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004294 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004295
smut@google.com34fb6b12015-07-13 20:03:26 +00004296 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004297 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004298 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004299
4300 if options.new_description:
4301 text = options.new_description
4302 if text == '-':
4303 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004304 elif text == '+':
4305 base_branch = cl.GetCommonAncestorWithUpstream()
4306 change = cl.GetChange(base_branch, None, local_description=True)
4307 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004308
4309 description.set_description(text)
4310 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004311 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004312
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004313 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004314 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004315 return 0
4316
4317
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004318@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004319def CMDlint(parser, args):
4320 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004321 parser.add_option('--filter', action='append', metavar='-x,+y',
4322 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004323 auth.add_auth_options(parser)
4324 options, args = parser.parse_args(args)
4325 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004326
4327 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004328 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004329 try:
4330 import cpplint
4331 import cpplint_chromium
4332 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004333 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004334 return 1
4335
4336 # Change the current working directory before calling lint so that it
4337 # shows the correct base.
4338 previous_cwd = os.getcwd()
4339 os.chdir(settings.GetRoot())
4340 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004341 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004342 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4343 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004344 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004345 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004346 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004347
4348 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004349 command = args + files
4350 if options.filter:
4351 command = ['--filter=' + ','.join(options.filter)] + command
4352 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004353
4354 white_regex = re.compile(settings.GetLintRegex())
4355 black_regex = re.compile(settings.GetLintIgnoreRegex())
4356 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4357 for filename in filenames:
4358 if white_regex.match(filename):
4359 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004360 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004361 else:
4362 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4363 extra_check_functions)
4364 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004365 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004366 finally:
4367 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004368 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004369 if cpplint._cpplint_state.error_count != 0:
4370 return 1
4371 return 0
4372
4373
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004374@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004375def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004376 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004377 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004378 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004379 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004380 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004381 parser.add_option('--all', action='store_true',
4382 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004383 parser.add_option('--parallel', action='store_true',
4384 help='Run all tests specified by input_api.RunTests in all '
4385 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004386 auth.add_auth_options(parser)
4387 options, args = parser.parse_args(args)
4388 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004389
sbc@chromium.org71437c02015-04-09 19:29:40 +00004390 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004391 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004392 return 1
4393
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004394 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004395 if args:
4396 base_branch = args[0]
4397 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004398 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004399 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004400
Aaron Gable8076c282017-11-29 14:39:41 -08004401 if options.all:
4402 base_change = cl.GetChange(base_branch, None)
4403 files = [('M', f) for f in base_change.AllFiles()]
4404 change = presubmit_support.GitChange(
4405 base_change.Name(),
4406 base_change.FullDescriptionText(),
4407 base_change.RepositoryRoot(),
4408 files,
4409 base_change.issue,
4410 base_change.patchset,
4411 base_change.author_email,
4412 base_change._upstream)
4413 else:
4414 change = cl.GetChange(base_branch, None)
4415
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004416 cl.RunHook(
4417 committing=not options.upload,
4418 may_prompt=False,
4419 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004420 change=change,
4421 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004422 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004423
4424
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004425def GenerateGerritChangeId(message):
4426 """Returns Ixxxxxx...xxx change id.
4427
4428 Works the same way as
4429 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4430 but can be called on demand on all platforms.
4431
4432 The basic idea is to generate git hash of a state of the tree, original commit
4433 message, author/committer info and timestamps.
4434 """
4435 lines = []
4436 tree_hash = RunGitSilent(['write-tree'])
4437 lines.append('tree %s' % tree_hash.strip())
4438 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4439 if code == 0:
4440 lines.append('parent %s' % parent.strip())
4441 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4442 lines.append('author %s' % author.strip())
4443 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4444 lines.append('committer %s' % committer.strip())
4445 lines.append('')
4446 # Note: Gerrit's commit-hook actually cleans message of some lines and
4447 # whitespace. This code is not doing this, but it clearly won't decrease
4448 # entropy.
4449 lines.append(message)
4450 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004451 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004452 return 'I%s' % change_hash.strip()
4453
4454
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004455def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004456 """Computes the remote branch ref to use for the CL.
4457
4458 Args:
4459 remote (str): The git remote for the CL.
4460 remote_branch (str): The git remote branch for the CL.
4461 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004462 """
4463 if not (remote and remote_branch):
4464 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004465
wittman@chromium.org455dc922015-01-26 20:15:50 +00004466 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004467 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004468 # refs, which are then translated into the remote full symbolic refs
4469 # below.
4470 if '/' not in target_branch:
4471 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4472 else:
4473 prefix_replacements = (
4474 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4475 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4476 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4477 )
4478 match = None
4479 for regex, replacement in prefix_replacements:
4480 match = re.search(regex, target_branch)
4481 if match:
4482 remote_branch = target_branch.replace(match.group(0), replacement)
4483 break
4484 if not match:
4485 # This is a branch path but not one we recognize; use as-is.
4486 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004487 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4488 # Handle the refs that need to land in different refs.
4489 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004490
wittman@chromium.org455dc922015-01-26 20:15:50 +00004491 # Create the true path to the remote branch.
4492 # Does the following translation:
4493 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4494 # * refs/remotes/origin/master -> refs/heads/master
4495 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4496 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4497 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4498 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4499 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4500 'refs/heads/')
4501 elif remote_branch.startswith('refs/remotes/branch-heads'):
4502 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004503
wittman@chromium.org455dc922015-01-26 20:15:50 +00004504 return remote_branch
4505
4506
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004507def cleanup_list(l):
4508 """Fixes a list so that comma separated items are put as individual items.
4509
4510 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4511 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4512 """
4513 items = sum((i.split(',') for i in l), [])
4514 stripped_items = (i.strip() for i in items)
4515 return sorted(filter(None, stripped_items))
4516
4517
Aaron Gable4db38df2017-11-03 14:59:07 -07004518@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004519@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004520def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004521 """Uploads the current changelist to codereview.
4522
4523 Can skip dependency patchset uploads for a branch by running:
4524 git config branch.branch_name.skip-deps-uploads True
4525 To unset run:
4526 git config --unset branch.branch_name.skip-deps-uploads
4527 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004528
4529 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4530 a bug number, this bug number is automatically populated in the CL
4531 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004532
4533 If subject contains text in square brackets or has "<text>: " prefix, such
4534 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4535 [git-cl] add support for hashtags
4536 Foo bar: implement foo
4537 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004538 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004539 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4540 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004541 parser.add_option('--bypass-watchlists', action='store_true',
4542 dest='bypass_watchlists',
4543 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004544 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004545 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004546 parser.add_option('--message', '-m', dest='message',
4547 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004548 parser.add_option('-b', '--bug',
4549 help='pre-populate the bug number(s) for this issue. '
4550 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004551 parser.add_option('--message-file', dest='message_file',
4552 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004553 parser.add_option('--title', '-t', dest='title',
4554 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004555 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004556 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004557 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004558 parser.add_option('--tbrs',
4559 action='append', default=[],
4560 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004561 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004562 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004563 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004564 parser.add_option('--hashtag', dest='hashtags',
4565 action='append', default=[],
4566 help=('Gerrit hashtag for new CL; '
4567 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004568 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004569 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004570 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004571 help='tell the commit queue to commit this patchset; '
4572 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004573 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004574 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004575 metavar='TARGET',
4576 help='Apply CL to remote ref TARGET. ' +
4577 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004578 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004579 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004580 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004581 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004582 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004583 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004584 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4585 const='TBR', help='add a set of OWNERS to TBR')
4586 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4587 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004588 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4589 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004590 help='Send the patchset to do a CQ dry run right after '
4591 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004592 parser.add_option('--dependencies', action='store_true',
4593 help='Uploads CLs of all the local branches that depend on '
4594 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004595 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4596 help='Sends your change to the CQ after an approval. Only '
4597 'works on repos that have the Auto-Submit label '
4598 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004599 parser.add_option('--parallel', action='store_true',
4600 help='Run all tests specified by input_api.RunTests in all '
4601 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004602
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004603 parser.add_option('--no-autocc', action='store_true',
4604 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004605 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004606 help='Set the review private. This implies --no-autocc.')
4607
rmistry@google.com2dd99862015-06-22 12:22:18 +00004608 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004609 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004610 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004611 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004612 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004613 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004614
sbc@chromium.org71437c02015-04-09 19:29:40 +00004615 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004616 return 1
4617
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004618 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004619 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004620 options.cc = cleanup_list(options.cc)
4621
tandriib80458a2016-06-23 12:20:07 -07004622 if options.message_file:
4623 if options.message:
4624 parser.error('only one of --message and --message-file allowed.')
4625 options.message = gclient_utils.FileRead(options.message_file)
4626 options.message_file = None
4627
tandrii4d0545a2016-07-06 03:56:49 -07004628 if options.cq_dry_run and options.use_commit_queue:
4629 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4630
Aaron Gableedbc4132017-09-11 13:22:28 -07004631 if options.use_commit_queue:
4632 options.send_mail = True
4633
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004634 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4635 settings.GetIsGerrit()
4636
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004637 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004638 if not cl.IsGerrit():
4639 # Error out with instructions for repos not yet configured for Gerrit.
4640 print('=====================================')
4641 print('NOTICE: Rietveld is no longer supported. '
4642 'You can upload changes to Gerrit with')
4643 print(' git cl upload --gerrit')
4644 print('or set Gerrit to be your default code review tool with')
4645 print(' git config gerrit.host true')
4646 print('=====================================')
4647 return 1
4648
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004649 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004650
4651
Francois Dorayd42c6812017-05-30 15:10:20 -04004652@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004653@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004654def CMDsplit(parser, args):
4655 """Splits a branch into smaller branches and uploads CLs.
4656
4657 Creates a branch and uploads a CL for each group of files modified in the
4658 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004659 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004660 the shared OWNERS file.
4661 """
4662 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004663 help="A text file containing a CL description in which "
4664 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004665 parser.add_option("-c", "--comment", dest="comment_file",
4666 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004667 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4668 default=False,
4669 help="List the files and reviewers for each CL that would "
4670 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004671 parser.add_option("--cq-dry-run", action='store_true',
4672 help="If set, will do a cq dry run for each uploaded CL. "
4673 "Please be careful when doing this; more than ~10 CLs "
4674 "has the potential to overload our build "
4675 "infrastructure. Try to upload these not during high "
4676 "load times (usually 11-3 Mountain View time). Email "
4677 "infra-dev@chromium.org with any questions.")
Takuto Ikuta51eca592019-02-14 19:40:52 +00004678 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4679 default=True,
4680 help='Sends your change to the CQ after an approval. Only '
4681 'works on repos that have the Auto-Submit label '
4682 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004683 options, _ = parser.parse_args(args)
4684
4685 if not options.description_file:
4686 parser.error('No --description flag specified.')
4687
4688 def WrappedCMDupload(args):
4689 return CMDupload(OptionParser(), args)
4690
4691 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004692 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004693 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004694
4695
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004696@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004697@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004698def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004699 """DEPRECATED: Used to commit the current changelist via git-svn."""
4700 message = ('git-cl no longer supports committing to SVN repositories via '
4701 'git-svn. You probably want to use `git cl land` instead.')
4702 print(message)
4703 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004704
4705
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004706# Two special branches used by git cl land.
4707MERGE_BRANCH = 'git-cl-commit'
4708CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4709
4710
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004711@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004712@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004713def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004714 """Commits the current changelist via git.
4715
4716 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4717 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004718 """
4719 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4720 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004721 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004722 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004723 parser.add_option('--parallel', action='store_true',
4724 help='Run all tests specified by input_api.RunTests in all '
4725 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004726 auth.add_auth_options(parser)
4727 (options, args) = parser.parse_args(args)
4728 auth_config = auth.extract_auth_config_from_options(options)
4729
4730 cl = Changelist(auth_config=auth_config)
4731
Robert Iannucci2e73d432018-03-14 01:10:47 -07004732 if not cl.IsGerrit():
4733 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004734
Robert Iannucci2e73d432018-03-14 01:10:47 -07004735 if not cl.GetIssue():
4736 DieWithError('You must upload the change first to Gerrit.\n'
4737 ' If you would rather have `git cl land` upload '
4738 'automatically for you, see http://crbug.com/642759')
4739 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004740 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004741
4742
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004743@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004744@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004745def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004746 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004747 parser.add_option('-b', dest='newbranch',
4748 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004749 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004750 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004751 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004752 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004753 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004754 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004755 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004756 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004757 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004758 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004759
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004760
4761 group = optparse.OptionGroup(
4762 parser,
4763 'Options for continuing work on the current issue uploaded from a '
4764 'different clone (e.g. different machine). Must be used independently '
4765 'from the other options. No issue number should be specified, and the '
4766 'branch must have an issue number associated with it')
4767 group.add_option('--reapply', action='store_true', dest='reapply',
4768 help='Reset the branch and reapply the issue.\n'
4769 'CAUTION: This will undo any local changes in this '
4770 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004771
4772 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004773 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004774 parser.add_option_group(group)
4775
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004776 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004777 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004778 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004779 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004780 auth_config = auth.extract_auth_config_from_options(options)
4781
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004782 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004783 if options.newbranch:
4784 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004785 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004786 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004787
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004788 cl = Changelist(auth_config=auth_config,
4789 codereview=options.forced_codereview)
4790 if not cl.GetIssue():
4791 parser.error('current branch must have an associated issue')
4792
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004793 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004794 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004795 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004796
4797 RunGit(['reset', '--hard', upstream])
4798 if options.pull:
4799 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004800
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004801 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4802 options.directory)
4803
4804 if len(args) != 1 or not args[0]:
4805 parser.error('Must specify issue number or url')
4806
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004807 target_issue_arg = ParseIssueNumberArgument(args[0],
4808 options.forced_codereview)
4809 if not target_issue_arg.valid:
4810 parser.error('invalid codereview url or CL id')
4811
4812 cl_kwargs = {
4813 'auth_config': auth_config,
4814 'codereview_host': target_issue_arg.hostname,
4815 'codereview': options.forced_codereview,
4816 }
4817 detected_codereview_from_url = False
4818 if target_issue_arg.codereview and not options.forced_codereview:
4819 detected_codereview_from_url = True
4820 cl_kwargs['codereview'] = target_issue_arg.codereview
4821 cl_kwargs['issue'] = target_issue_arg.issue
4822
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004823 # We don't want uncommitted changes mixed up with the patch.
4824 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004825 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004826
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004827 if options.newbranch:
4828 if options.force:
4829 RunGit(['branch', '-D', options.newbranch],
4830 stderr=subprocess2.PIPE, error_ok=True)
4831 RunGit(['new-branch', options.newbranch])
4832
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004833 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004834
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004835 if cl.IsGerrit():
4836 if options.reject:
4837 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004838 if options.directory:
4839 parser.error('--directory is not supported with Gerrit codereview.')
4840
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004841 if detected_codereview_from_url:
4842 print('canonical issue/change URL: %s (type: %s)\n' %
4843 (cl.GetIssueURL(), target_issue_arg.codereview))
4844
4845 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07004846 options.nocommit, options.directory,
4847 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004848
4849
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004850def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004851 """Fetches the tree status and returns either 'open', 'closed',
4852 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004853 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004854 if url:
4855 status = urllib2.urlopen(url).read().lower()
4856 if status.find('closed') != -1 or status == '0':
4857 return 'closed'
4858 elif status.find('open') != -1 or status == '1':
4859 return 'open'
4860 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004861 return 'unset'
4862
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004863
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004864def GetTreeStatusReason():
4865 """Fetches the tree status from a json url and returns the message
4866 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004867 url = settings.GetTreeStatusUrl()
4868 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004869 connection = urllib2.urlopen(json_url)
4870 status = json.loads(connection.read())
4871 connection.close()
4872 return status['message']
4873
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004874
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004875@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004876def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004877 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004878 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004879 status = GetTreeStatus()
4880 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004881 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004882 return 2
4883
vapiera7fbd5a2016-06-16 09:17:49 -07004884 print('The tree is %s' % status)
4885 print()
4886 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004887 if status != 'open':
4888 return 1
4889 return 0
4890
4891
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004892@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004893def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004894 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004895 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004896 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004897 '-b', '--bot', action='append',
4898 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4899 'times to specify multiple builders. ex: '
4900 '"-b win_rel -b win_layout". See '
4901 'the try server waterfall for the builders name and the tests '
4902 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004903 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004904 '-B', '--bucket', default='',
4905 help=('Buildbucket bucket to send the try requests.'))
4906 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004907 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004908 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004909 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004910 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004911 help='Revision to use for the try job; default: the revision will '
4912 'be determined by the try recipe that builder runs, which usually '
4913 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004914 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004915 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004916 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004917 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004918 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004919 '--category', default='git_cl_try', help='Specify custom build category.')
4920 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004921 '--project',
4922 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004923 'in recipe to determine to which repository or directory to '
4924 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004925 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004926 '-p', '--property', dest='properties', action='append', default=[],
4927 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004928 'key2=value2 etc. The value will be treated as '
4929 'json if decodable, or as string otherwise. '
4930 'NOTE: using this may make your try job not usable for CQ, '
4931 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004932 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004933 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4934 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004935 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004936 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09004937 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004938 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09004939 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004940 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004941
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004942 if options.master and options.master.startswith('luci.'):
4943 parser.error(
4944 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00004945 # Make sure that all properties are prop=value pairs.
4946 bad_params = [x for x in options.properties if '=' not in x]
4947 if bad_params:
4948 parser.error('Got properties with missing "=": %s' % bad_params)
4949
maruel@chromium.org15192402012-09-06 12:38:29 +00004950 if args:
4951 parser.error('Unknown arguments: %s' % args)
4952
Koji Ishii31c14782018-01-08 17:17:33 +09004953 cl = Changelist(auth_config=auth_config, issue=options.issue,
4954 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00004955 if not cl.GetIssue():
4956 parser.error('Need to upload first')
4957
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004958 if cl.IsGerrit():
4959 # HACK: warm up Gerrit change detail cache to save on RPCs.
4960 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4961
tandriie113dfd2016-10-11 10:20:12 -07004962 error_message = cl.CannotTriggerTryJobReason()
4963 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004964 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004965
borenet6c0efe62016-10-19 08:13:29 -07004966 if options.bucket and options.master:
4967 parser.error('Only one of --bucket and --master may be used.')
4968
qyearsley1fdfcb62016-10-24 13:22:03 -07004969 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004970
qyearsleydd49f942016-10-28 11:57:22 -07004971 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4972 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004973 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004974 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004975 print('git cl try with no bots now defaults to CQ dry run.')
4976 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4977 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004978
borenet6c0efe62016-10-19 08:13:29 -07004979 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004980 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004981 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004982 'of bot requires an initial job from a parent (usually a builder). '
4983 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004984 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004985 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004986
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004987 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07004988 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004989 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07004990 except BuildbucketResponseException as ex:
4991 print('ERROR: %s' % ex)
4992 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004993 return 0
4994
4995
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004996@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004997def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004998 """Prints info about try jobs associated with current CL."""
4999 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005000 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005001 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005002 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005003 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005004 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005005 '--color', action='store_true', default=setup_color.IS_TTY,
5006 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005007 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005008 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5009 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005010 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005011 '--json', help=('Path of JSON output file to write try job results to,'
5012 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005013 parser.add_option_group(group)
5014 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005015 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005016 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005017 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005018 if args:
5019 parser.error('Unrecognized args: %s' % ' '.join(args))
5020
5021 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005022 cl = Changelist(
5023 issue=options.issue, codereview=options.forced_codereview,
5024 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005025 if not cl.GetIssue():
5026 parser.error('Need to upload first')
5027
tandrii221ab252016-10-06 08:12:04 -07005028 patchset = options.patchset
5029 if not patchset:
5030 patchset = cl.GetMostRecentPatchset()
5031 if not patchset:
5032 parser.error('Codereview doesn\'t know about issue %s. '
5033 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005034 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005035 cl.GetIssue())
5036
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005037 try:
tandrii221ab252016-10-06 08:12:04 -07005038 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005039 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005040 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005041 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005042 if options.json:
5043 write_try_results_json(options.json, jobs)
5044 else:
5045 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005046 return 0
5047
5048
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005049@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005050@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005051def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005052 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005053 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005054 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005055 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005056
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005057 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005058 if args:
5059 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005060 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005061 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005062 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005063 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005064
5065 # Clear configured merge-base, if there is one.
5066 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005067 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005068 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005069 return 0
5070
5071
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005072@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005073def CMDweb(parser, args):
5074 """Opens the current CL in the web browser."""
5075 _, args = parser.parse_args(args)
5076 if args:
5077 parser.error('Unrecognized args: %s' % ' '.join(args))
5078
5079 issue_url = Changelist().GetIssueURL()
5080 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005081 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005082 return 1
5083
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005084 # Redirect I/O before invoking browser to hide its output. For example, this
5085 # allows to hide "Created new window in existing browser session." message
5086 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5087 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005088 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005089 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005090 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005091 os.open(os.devnull, os.O_RDWR)
5092 try:
5093 webbrowser.open(issue_url)
5094 finally:
5095 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005096 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005097 return 0
5098
5099
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005100@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005101def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005102 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005103 parser.add_option('-d', '--dry-run', action='store_true',
5104 help='trigger in dry run mode')
5105 parser.add_option('-c', '--clear', action='store_true',
5106 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005107 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005108 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005109 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005110 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005111 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005112 if args:
5113 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005114 if options.dry_run and options.clear:
5115 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5116
iannuccie53c9352016-08-17 14:40:40 -07005117 cl = Changelist(auth_config=auth_config, issue=options.issue,
5118 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005119 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005120 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005121 elif options.dry_run:
5122 state = _CQState.DRY_RUN
5123 else:
5124 state = _CQState.COMMIT
5125 if not cl.GetIssue():
5126 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005127 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005128 return 0
5129
5130
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005131@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005132def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005133 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005134 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005135 auth.add_auth_options(parser)
5136 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005137 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005138 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005139 if args:
5140 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005141 cl = Changelist(auth_config=auth_config, issue=options.issue,
5142 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005143 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005144 if not cl.GetIssue():
5145 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005146 cl.CloseIssue()
5147 return 0
5148
5149
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005150@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005151def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005152 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005153 parser.add_option(
5154 '--stat',
5155 action='store_true',
5156 dest='stat',
5157 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005158 auth.add_auth_options(parser)
5159 options, args = parser.parse_args(args)
5160 auth_config = auth.extract_auth_config_from_options(options)
5161 if args:
5162 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005163
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005164 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005165 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005166 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005167 if not issue:
5168 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005169
Aaron Gablea718c3e2017-08-28 17:47:28 -07005170 base = cl._GitGetBranchConfigValue('last-upload-hash')
5171 if not base:
5172 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5173 if not base:
5174 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5175 revision_info = detail['revisions'][detail['current_revision']]
5176 fetch_info = revision_info['fetch']['http']
5177 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5178 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005179
Aaron Gablea718c3e2017-08-28 17:47:28 -07005180 cmd = ['git', 'diff']
5181 if options.stat:
5182 cmd.append('--stat')
5183 cmd.append(base)
5184 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005185
5186 return 0
5187
5188
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005189@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005190def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005191 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005192 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005193 '--ignore-current',
5194 action='store_true',
5195 help='Ignore the CL\'s current reviewers and start from scratch.')
5196 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005197 '--ignore-self',
5198 action='store_true',
5199 help='Do not consider CL\'s author as an owners.')
5200 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005201 '--no-color',
5202 action='store_true',
5203 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005204 parser.add_option(
5205 '--batch',
5206 action='store_true',
5207 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005208 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005209 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005210 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005211
5212 author = RunGit(['config', 'user.email']).strip() or None
5213
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005214 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005215
5216 if args:
5217 if len(args) > 1:
5218 parser.error('Unknown args')
5219 base_branch = args[0]
5220 else:
5221 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005222 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005223
5224 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005225 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5226
5227 if options.batch:
5228 db = owners.Database(change.RepositoryRoot(), file, os.path)
5229 print('\n'.join(db.reviewers_for(affected_files, author)))
5230 return 0
5231
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005232 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005233 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005234 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005235 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005236 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005237 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005238 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005239 override_files=change.OriginalOwnersFiles(),
5240 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005241
5242
Aiden Bennerc08566e2018-10-03 17:52:42 +00005243def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005244 """Generates a diff command."""
5245 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005246 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5247
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005248 if allow_prefix:
5249 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5250 # case that diff.noprefix is set in the user's git config.
5251 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5252 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005253 diff_cmd += ['--no-prefix']
5254
5255 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005256
5257 if args:
5258 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005259 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005260 diff_cmd.append(arg)
5261 else:
5262 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005263
5264 return diff_cmd
5265
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005266
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005267def MatchingFileType(file_name, extensions):
5268 """Returns true if the file name ends with one of the given extensions."""
5269 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005270
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005271
enne@chromium.org555cfe42014-01-29 18:21:39 +00005272@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005273@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005274def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005275 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005276 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005277 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005278 parser.add_option('--full', action='store_true',
5279 help='Reformat the full content of all touched files')
5280 parser.add_option('--dry-run', action='store_true',
5281 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005282 parser.add_option(
5283 '--python',
5284 action='store_true',
5285 default=None,
5286 help='Enables python formatting on all python files.')
5287 parser.add_option(
5288 '--no-python',
5289 action='store_true',
5290 dest='python',
5291 help='Disables python formatting on all python files. '
5292 'Takes precedence over --python. '
5293 'If neither --python or --no-python are set, python '
5294 'files that have a .style.yapf file in an ancestor '
5295 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005296 parser.add_option('--js', action='store_true',
5297 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005298 parser.add_option('--diff', action='store_true',
5299 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005300 parser.add_option('--presubmit', action='store_true',
5301 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005302 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005303
Daniel Chengc55eecf2016-12-30 03:11:02 -08005304 # Normalize any remaining args against the current path, so paths relative to
5305 # the current directory are still resolved as expected.
5306 args = [os.path.join(os.getcwd(), arg) for arg in args]
5307
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005308 # git diff generates paths against the root of the repository. Change
5309 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005310 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005311 if rel_base_path:
5312 os.chdir(rel_base_path)
5313
digit@chromium.org29e47272013-05-17 17:01:46 +00005314 # Grab the merge-base commit, i.e. the upstream commit of the current
5315 # branch when it was created or the last time it was rebased. This is
5316 # to cover the case where the user may have called "git fetch origin",
5317 # moving the origin branch to a newer commit, but hasn't rebased yet.
5318 upstream_commit = None
5319 cl = Changelist()
5320 upstream_branch = cl.GetUpstreamBranch()
5321 if upstream_branch:
5322 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5323 upstream_commit = upstream_commit.strip()
5324
5325 if not upstream_commit:
5326 DieWithError('Could not find base commit for this branch. '
5327 'Are you in detached state?')
5328
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005329 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5330 diff_output = RunGit(changed_files_cmd)
5331 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005332 # Filter out files deleted by this CL
5333 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005334
Christopher Lamc5ba6922017-01-24 11:19:14 +11005335 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005336 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005337
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005338 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5339 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5340 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005341 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005342
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005343 top_dir = os.path.normpath(
5344 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5345
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005346 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5347 # formatted. This is used to block during the presubmit.
5348 return_value = 0
5349
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005350 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005351 # Locate the clang-format binary in the checkout
5352 try:
5353 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005354 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005355 DieWithError(e)
5356
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005357 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005358 cmd = [clang_format_tool]
5359 if not opts.dry_run and not opts.diff:
5360 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005361 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005362 if opts.diff:
5363 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005364 else:
5365 env = os.environ.copy()
5366 env['PATH'] = str(os.path.dirname(clang_format_tool))
5367 try:
5368 script = clang_format.FindClangFormatScriptInChromiumTree(
5369 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005370 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005371 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005372
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005373 cmd = [sys.executable, script, '-p0']
5374 if not opts.dry_run and not opts.diff:
5375 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005376
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005377 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5378 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005379
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005380 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5381 if opts.diff:
5382 sys.stdout.write(stdout)
5383 if opts.dry_run and len(stdout) > 0:
5384 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005385
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005386 # Similar code to above, but using yapf on .py files rather than clang-format
5387 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005388 py_explicitly_disabled = opts.python is not None and not opts.python
5389 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005390 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5391 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5392 if sys.platform.startswith('win'):
5393 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005394
Aiden Bennerc08566e2018-10-03 17:52:42 +00005395 # If we couldn't find a yapf file we'll default to the chromium style
5396 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005397 chromium_default_yapf_style = os.path.join(depot_tools_path,
5398 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005399 # Used for caching.
5400 yapf_configs = {}
5401 for f in python_diff_files:
5402 # Find the yapf style config for the current file, defaults to depot
5403 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005404 _FindYapfConfigFile(f, yapf_configs, top_dir)
5405
5406 # Turn on python formatting by default if a yapf config is specified.
5407 # This breaks in the case of this repo though since the specified
5408 # style file is also the global default.
5409 if opts.python is None:
5410 filtered_py_files = []
5411 for f in python_diff_files:
5412 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5413 filtered_py_files.append(f)
5414 else:
5415 filtered_py_files = python_diff_files
5416
5417 # Note: yapf still seems to fix indentation of the entire file
5418 # even if line ranges are specified.
5419 # See https://github.com/google/yapf/issues/499
5420 if not opts.full and filtered_py_files:
5421 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5422
5423 for f in filtered_py_files:
5424 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5425 if yapf_config is None:
5426 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005427
5428 cmd = [yapf_tool, '--style', yapf_config, f]
5429
5430 has_formattable_lines = False
5431 if not opts.full:
5432 # Only run yapf over changed line ranges.
5433 for diff_start, diff_len in py_line_diffs[f]:
5434 diff_end = diff_start + diff_len - 1
5435 # Yapf errors out if diff_end < diff_start but this
5436 # is a valid line range diff for a removal.
5437 if diff_end >= diff_start:
5438 has_formattable_lines = True
5439 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5440 # If all line diffs were removals we have nothing to format.
5441 if not has_formattable_lines:
5442 continue
5443
5444 if opts.diff or opts.dry_run:
5445 cmd += ['--diff']
5446 # Will return non-zero exit code if non-empty diff.
5447 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5448 if opts.diff:
5449 sys.stdout.write(stdout)
5450 elif len(stdout) > 0:
5451 return_value = 2
5452 else:
5453 cmd += ['-i']
5454 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005455
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005456 # Dart's formatter does not have the nice property of only operating on
5457 # modified chunks, so hard code full.
5458 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005459 try:
5460 command = [dart_format.FindDartFmtToolInChromiumTree()]
5461 if not opts.dry_run and not opts.diff:
5462 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005463 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005464
ppi@chromium.org6593d932016-03-03 15:41:15 +00005465 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005466 if opts.dry_run and stdout:
5467 return_value = 2
5468 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005469 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5470 'found in this checkout. Files in other languages are still '
5471 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005472
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005473 # Format GN build files. Always run on full build files for canonical form.
5474 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005475 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005476 if opts.dry_run or opts.diff:
5477 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005478 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005479 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5480 shell=sys.platform == 'win32',
5481 cwd=top_dir)
5482 if opts.dry_run and gn_ret == 2:
5483 return_value = 2 # Not formatted.
5484 elif opts.diff and gn_ret == 2:
5485 # TODO this should compute and print the actual diff.
5486 print("This change has GN build file diff for " + gn_diff_file)
5487 elif gn_ret != 0:
5488 # For non-dry run cases (and non-2 return values for dry-run), a
5489 # nonzero error code indicates a failure, probably because the file
5490 # doesn't parse.
5491 DieWithError("gn format failed on " + gn_diff_file +
5492 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005493
Ilya Shermane081cbe2017-08-15 17:51:04 -07005494 # Skip the metrics formatting from the global presubmit hook. These files have
5495 # a separate presubmit hook that issues an error if the files need formatting,
5496 # whereas the top-level presubmit script merely issues a warning. Formatting
5497 # these files is somewhat slow, so it's important not to duplicate the work.
5498 if not opts.presubmit:
5499 for xml_dir in GetDirtyMetricsDirs(diff_files):
5500 tool_dir = os.path.join(top_dir, xml_dir)
5501 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5502 if opts.dry_run or opts.diff:
5503 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005504 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005505 if opts.diff:
5506 sys.stdout.write(stdout)
5507 if opts.dry_run and stdout:
5508 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005509
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005510 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005511
Steven Holte2e664bf2017-04-21 13:10:47 -07005512def GetDirtyMetricsDirs(diff_files):
5513 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5514 metrics_xml_dirs = [
5515 os.path.join('tools', 'metrics', 'actions'),
5516 os.path.join('tools', 'metrics', 'histograms'),
5517 os.path.join('tools', 'metrics', 'rappor'),
5518 os.path.join('tools', 'metrics', 'ukm')]
5519 for xml_dir in metrics_xml_dirs:
5520 if any(file.startswith(xml_dir) for file in xml_diff_files):
5521 yield xml_dir
5522
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005523
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005524@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005525@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005526def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005527 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005528 _, args = parser.parse_args(args)
5529
5530 if len(args) != 1:
5531 parser.print_help()
5532 return 1
5533
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005534 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005535 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005536 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005537
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005538 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005539
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005540 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005541 output = RunGit(['config', '--local', '--get-regexp',
5542 r'branch\..*\.%s' % issueprefix],
5543 error_ok=True)
5544 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005545 if issue == target_issue:
5546 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005547
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005548 branches = []
5549 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005550 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005551 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005552 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005553 return 1
5554 if len(branches) == 1:
5555 RunGit(['checkout', branches[0]])
5556 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005557 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005558 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005559 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005560 which = raw_input('Choose by index: ')
5561 try:
5562 RunGit(['checkout', branches[int(which)]])
5563 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005564 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005565 return 1
5566
5567 return 0
5568
5569
maruel@chromium.org29404b52014-09-08 22:58:00 +00005570def CMDlol(parser, args):
5571 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005572 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005573 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5574 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5575 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005576 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005577 return 0
5578
5579
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005580class OptionParser(optparse.OptionParser):
5581 """Creates the option parse and add --verbose support."""
5582 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005583 optparse.OptionParser.__init__(
5584 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005585 self.add_option(
5586 '-v', '--verbose', action='count', default=0,
5587 help='Use 2 times for more debugging info')
5588
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005589 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005590 try:
5591 return self._parse_args(args)
5592 finally:
5593 # Regardless of success or failure of args parsing, we want to report
5594 # metrics, but only after logging has been initialized (if parsing
5595 # succeeded).
5596 global settings
5597 settings = Settings()
5598
5599 if not metrics.DISABLE_METRICS_COLLECTION:
5600 # GetViewVCUrl ultimately calls logging method.
5601 project_url = settings.GetViewVCUrl().strip('/+')
5602 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5603 metrics.collector.add('project_urls', [project_url])
5604
5605 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005606 # Create an optparse.Values object that will store only the actual passed
5607 # options, without the defaults.
5608 actual_options = optparse.Values()
5609 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5610 # Create an optparse.Values object with the default options.
5611 options = optparse.Values(self.get_default_values().__dict__)
5612 # Update it with the options passed by the user.
5613 options._update_careful(actual_options.__dict__)
5614 # Store the options passed by the user in an _actual_options attribute.
5615 # We store only the keys, and not the values, since the values can contain
5616 # arbitrary information, which might be PII.
5617 metrics.collector.add('arguments', actual_options.__dict__.keys())
5618
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005619 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005620 logging.basicConfig(
5621 level=levels[min(options.verbose, len(levels) - 1)],
5622 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5623 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005624
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005625 return options, args
5626
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005628def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005629 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005630 print('\nYour python version %s is unsupported, please upgrade.\n' %
5631 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005632 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005633
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005634 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005635 dispatcher = subcommand.CommandDispatcher(__name__)
5636 try:
5637 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005638 except auth.AuthenticationError as e:
5639 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005640 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005641 if e.code != 500:
5642 raise
5643 DieWithError(
5644 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5645 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005646 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005647
5648
5649if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005650 # These affect sys.stdout so do it outside of main() to simplify mocks in
5651 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005652 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005653 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005654 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005655 sys.exit(main(sys.argv[1:]))