blob: a981b6e4a4646090fc1187cd4dfc2096b7c87720 [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:
911 self.git_editor = self._GetConfig('core.editor', error_ok=True)
912 return self.git_editor or None
913
thestig@chromium.org44202a22014-03-11 19:22:18 +0000914 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000915 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000916 DEFAULT_LINT_REGEX)
917
918 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000919 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000920 DEFAULT_LINT_IGNORE_REGEX)
921
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922 def _GetConfig(self, param, **kwargs):
923 self.LazyUpdateIfNeeded()
924 return RunGit(['config', param], **kwargs).strip()
925
926
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100927@contextlib.contextmanager
928def _get_gerrit_project_config_file(remote_url):
929 """Context manager to fetch and store Gerrit's project.config from
930 refs/meta/config branch and store it in temp file.
931
932 Provides a temporary filename or None if there was error.
933 """
934 error, _ = RunGitWithCode([
935 'fetch', remote_url,
936 '+refs/meta/config:refs/git_cl/meta/config'])
937 if error:
938 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700939 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100940 (remote_url, error))
941 yield None
942 return
943
944 error, project_config_data = RunGitWithCode(
945 ['show', 'refs/git_cl/meta/config:project.config'])
946 if error:
947 print('WARNING: project.config file not found')
948 yield None
949 return
950
951 with gclient_utils.temporary_directory() as tempdir:
952 project_config_file = os.path.join(tempdir, 'project.config')
953 gclient_utils.FileWrite(project_config_file, project_config_data)
954 yield project_config_file
955
956
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957def ShortBranchName(branch):
958 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000959 return branch.replace('refs/heads/', '', 1)
960
961
962def GetCurrentBranchRef():
963 """Returns branch ref (e.g., refs/heads/master) or None."""
964 return RunGit(['symbolic-ref', 'HEAD'],
965 stderr=subprocess2.VOID, error_ok=True).strip() or None
966
967
968def GetCurrentBranch():
969 """Returns current branch or None.
970
971 For refs/heads/* branches, returns just last part. For others, full ref.
972 """
973 branchref = GetCurrentBranchRef()
974 if branchref:
975 return ShortBranchName(branchref)
976 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977
978
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000979class _CQState(object):
980 """Enum for states of CL with respect to Commit Queue."""
981 NONE = 'none'
982 DRY_RUN = 'dry_run'
983 COMMIT = 'commit'
984
985 ALL_STATES = [NONE, DRY_RUN, COMMIT]
986
987
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000988class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200989 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000990 self.issue = issue
991 self.patchset = patchset
992 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +0000993 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200994 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000995
996 @property
997 def valid(self):
998 return self.issue is not None
999
1000
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001001def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001002 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1003 fail_result = _ParsedIssueNumberArgument()
1004
1005 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001006 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001007 if not arg.startswith('http'):
1008 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001009
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001010 url = gclient_utils.UpgradeToHttps(arg)
1011 try:
1012 parsed_url = urlparse.urlparse(url)
1013 except ValueError:
1014 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001015
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001016 if codereview is not None:
1017 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1018 return parsed or fail_result
1019
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001020 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001021
1022
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001023def _create_description_from_log(args):
1024 """Pulls out the commit log to use as a base for the CL description."""
1025 log_args = []
1026 if len(args) == 1 and not args[0].endswith('.'):
1027 log_args = [args[0] + '..']
1028 elif len(args) == 1 and args[0].endswith('...'):
1029 log_args = [args[0][:-1]]
1030 elif len(args) == 2:
1031 log_args = [args[0] + '..' + args[1]]
1032 else:
1033 log_args = args[:] # Hope for the best!
1034 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1035
1036
Aaron Gablea45ee112016-11-22 15:14:38 -08001037class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001038 def __init__(self, issue, url):
1039 self.issue = issue
1040 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001041 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001042
1043 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001044 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001045 self.issue, self.url)
1046
1047
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001048_CommentSummary = collections.namedtuple(
1049 '_CommentSummary', ['date', 'message', 'sender',
1050 # TODO(tandrii): these two aren't known in Gerrit.
1051 'approval', 'disapproval'])
1052
1053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001055 """Changelist works with one changelist in local branch.
1056
1057 Supports two codereview backends: Rietveld or Gerrit, selected at object
1058 creation.
1059
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001060 Notes:
1061 * Not safe for concurrent multi-{thread,process} use.
1062 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001063 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001064 """
1065
1066 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1067 """Create a new ChangeList instance.
1068
1069 If issue is given, the codereview must be given too.
1070
1071 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1072 Otherwise, it's decided based on current configuration of the local branch,
1073 with default being 'rietveld' for backwards compatibility.
1074 See _load_codereview_impl for more details.
1075
1076 **kwargs will be passed directly to codereview implementation.
1077 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001079 global settings
1080 if not settings:
1081 # Happens when git_cl.py is used as a utility library.
1082 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001083
1084 if issue:
1085 assert codereview, 'codereview must be known, if issue is known'
1086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087 self.branchref = branchref
1088 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001089 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 self.branch = ShortBranchName(self.branchref)
1091 else:
1092 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001094 self.lookedup_issue = False
1095 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001096 self.has_description = False
1097 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001098 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001100 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001101 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001102 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001103 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001104
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001105 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001106 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001107 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001108 assert self._codereview_impl
1109 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001110
1111 def _load_codereview_impl(self, codereview=None, **kwargs):
1112 if codereview:
Joe Masond87b0962018-12-03 21:04:46 +00001113 assert codereview in _CODEREVIEW_IMPLEMENTATIONS, (
1114 'codereview {} not in {}'.format(codereview,
1115 _CODEREVIEW_IMPLEMENTATIONS))
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001116 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1117 self._codereview = codereview
1118 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001119 return
1120
1121 # Automatic selection based on issue number set for a current branch.
1122 # Rietveld takes precedence over Gerrit.
1123 assert not self.issue
1124 # Whether we find issue or not, we are doing the lookup.
1125 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001126 if self.GetBranch():
1127 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1128 issue = _git_get_branch_config_value(
1129 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1130 if issue:
1131 self._codereview = codereview
1132 self._codereview_impl = cls(self, **kwargs)
1133 self.issue = int(issue)
1134 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001135
1136 # No issue is set for this branch, so decide based on repo-wide settings.
1137 return self._load_codereview_impl(
1138 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1139 **kwargs)
1140
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001141 def IsGerrit(self):
1142 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001143
1144 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001145 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001146
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001147 The return value is a string suitable for passing to git cl with the --cc
1148 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001149 """
1150 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001151 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001152 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1154 return self.cc
1155
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001156 def GetCCListWithoutDefault(self):
1157 """Return the users cc'd on this CL excluding default ones."""
1158 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001159 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001160 return self.cc
1161
Daniel Cheng7227d212017-11-17 08:12:37 -08001162 def ExtendCC(self, more_cc):
1163 """Extends the list of users to cc on this CL based on the changed files."""
1164 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165
1166 def GetBranch(self):
1167 """Returns the short branch name, e.g. 'master'."""
1168 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001170 if not branchref:
1171 return None
1172 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 self.branch = ShortBranchName(self.branchref)
1174 return self.branch
1175
1176 def GetBranchRef(self):
1177 """Returns the full branch name, e.g. 'refs/heads/master'."""
1178 self.GetBranch() # Poke the lazy loader.
1179 return self.branchref
1180
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001181 def ClearBranch(self):
1182 """Clears cached branch data of this object."""
1183 self.branch = self.branchref = None
1184
tandrii5d48c322016-08-18 16:19:37 -07001185 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1186 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1187 kwargs['branch'] = self.GetBranch()
1188 return _git_get_branch_config_value(key, default, **kwargs)
1189
1190 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1191 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1192 assert self.GetBranch(), (
1193 'this CL must have an associated branch to %sset %s%s' %
1194 ('un' if value is None else '',
1195 key,
1196 '' if value is None else ' to %r' % value))
1197 kwargs['branch'] = self.GetBranch()
1198 return _git_set_branch_config_value(key, value, **kwargs)
1199
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001200 @staticmethod
1201 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001202 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203 e.g. 'origin', 'refs/heads/master'
1204 """
1205 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001206 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1207
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001209 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001211 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1212 error_ok=True).strip()
1213 if upstream_branch:
1214 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001216 # Else, try to guess the origin remote.
1217 remote_branches = RunGit(['branch', '-r']).split()
1218 if 'origin/master' in remote_branches:
1219 # Fall back on origin/master if it exits.
1220 remote = 'origin'
1221 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001223 DieWithError(
1224 'Unable to determine default branch to diff against.\n'
1225 'Either pass complete "git diff"-style arguments, like\n'
1226 ' git cl upload origin/master\n'
1227 'or verify this branch is set up to track another \n'
1228 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229
1230 return remote, upstream_branch
1231
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001232 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001233 upstream_branch = self.GetUpstreamBranch()
1234 if not BranchExists(upstream_branch):
1235 DieWithError('The upstream for the current branch (%s) does not exist '
1236 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001237 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001238 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 def GetUpstreamBranch(self):
1241 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001242 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001244 upstream_branch = upstream_branch.replace('refs/heads/',
1245 'refs/remotes/%s/' % remote)
1246 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1247 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 self.upstream_branch = upstream_branch
1249 return self.upstream_branch
1250
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001251 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001252 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001253 remote, branch = None, self.GetBranch()
1254 seen_branches = set()
1255 while branch not in seen_branches:
1256 seen_branches.add(branch)
1257 remote, branch = self.FetchUpstreamTuple(branch)
1258 branch = ShortBranchName(branch)
1259 if remote != '.' or branch.startswith('refs/remotes'):
1260 break
1261 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001262 remotes = RunGit(['remote'], error_ok=True).split()
1263 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001264 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001265 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001266 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001267 logging.warn('Could not determine which remote this change is '
1268 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001269 else:
1270 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001271 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001272 branch = 'HEAD'
1273 if branch.startswith('refs/remotes'):
1274 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001275 elif branch.startswith('refs/branch-heads/'):
1276 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001277 else:
1278 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001279 return self._remote
1280
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001281 def GitSanityChecks(self, upstream_git_obj):
1282 """Checks git repo status and ensures diff is from local commits."""
1283
sbc@chromium.org79706062015-01-14 21:18:12 +00001284 if upstream_git_obj is None:
1285 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001286 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001287 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001288 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001289 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001290 return False
1291
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001292 # Verify the commit we're diffing against is in our current branch.
1293 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1294 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1295 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001296 print('ERROR: %s is not in the current branch. You may need to rebase '
1297 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001298 return False
1299
1300 # List the commits inside the diff, and verify they are all local.
1301 commits_in_diff = RunGit(
1302 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1303 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1304 remote_branch = remote_branch.strip()
1305 if code != 0:
1306 _, remote_branch = self.GetRemoteBranch()
1307
1308 commits_in_remote = RunGit(
1309 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1310
1311 common_commits = set(commits_in_diff) & set(commits_in_remote)
1312 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001313 print('ERROR: Your diff contains %d commits already in %s.\n'
1314 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1315 'the diff. If you are using a custom git flow, you can override'
1316 ' the reference used for this check with "git config '
1317 'gitcl.remotebranch <git-ref>".' % (
1318 len(common_commits), remote_branch, upstream_git_obj),
1319 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001320 return False
1321 return True
1322
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001323 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001324 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001325
1326 Returns None if it is not set.
1327 """
tandrii5d48c322016-08-18 16:19:37 -07001328 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001329
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 def GetRemoteUrl(self):
1331 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1332
1333 Returns None if there is no remote.
1334 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001335 is_cached, value = self._cached_remote_url
1336 if is_cached:
1337 return value
1338
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001339 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001340 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1341
1342 # If URL is pointing to a local directory, it is probably a git cache.
1343 if os.path.isdir(url):
1344 url = RunGit(['config', 'remote.%s.url' % remote],
1345 error_ok=True,
1346 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001347 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001348 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001350 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001351 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001352 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001353 self.issue = self._GitGetBranchConfigValue(
1354 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001355 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 return self.issue
1357
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358 def GetIssueURL(self):
1359 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001360 issue = self.GetIssue()
1361 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001362 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001363 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001364
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001365 def GetDescription(self, pretty=False, force=False):
1366 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001368 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 self.has_description = True
1370 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001371 # Set width to 72 columns + 2 space indent.
1372 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001374 lines = self.description.splitlines()
1375 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001376 return self.description
1377
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001378 def GetDescriptionFooters(self):
1379 """Returns (non_footer_lines, footers) for the commit message.
1380
1381 Returns:
1382 non_footer_lines (list(str)) - Simple list of description lines without
1383 any footer. The lines do not contain newlines, nor does the list contain
1384 the empty line between the message and the footers.
1385 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1386 [("Change-Id", "Ideadbeef...."), ...]
1387 """
1388 raw_description = self.GetDescription()
1389 msg_lines, _, footers = git_footers.split_footers(raw_description)
1390 if footers:
1391 msg_lines = msg_lines[:len(msg_lines)-1]
1392 return msg_lines, footers
1393
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001395 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001396 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001397 self.patchset = self._GitGetBranchConfigValue(
1398 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001399 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400 return self.patchset
1401
1402 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001403 """Set this branch's patchset. If patchset=0, clears the patchset."""
1404 assert self.GetBranch()
1405 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001406 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001407 else:
1408 self.patchset = int(patchset)
1409 self._GitSetBranchConfigValue(
1410 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001412 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001413 """Set this branch's issue. If issue isn't given, clears the issue."""
1414 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001416 issue = int(issue)
1417 self._GitSetBranchConfigValue(
1418 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001419 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001420 codereview_server = self._codereview_impl.GetCodereviewServer()
1421 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001422 self._GitSetBranchConfigValue(
1423 self._codereview_impl.CodereviewServerConfigKey(),
1424 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 else:
tandrii5d48c322016-08-18 16:19:37 -07001426 # Reset all of these just to be clean.
1427 reset_suffixes = [
1428 'last-upload-hash',
1429 self._codereview_impl.IssueConfigKey(),
1430 self._codereview_impl.PatchsetConfigKey(),
1431 self._codereview_impl.CodereviewServerConfigKey(),
1432 ] + self._PostUnsetIssueProperties()
1433 for prop in reset_suffixes:
1434 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001435 msg = RunGit(['log', '-1', '--format=%B']).strip()
1436 if msg and git_footers.get_footer_change_id(msg):
1437 print('WARNING: The change patched into this branch has a Change-Id. '
1438 'Removing it.')
1439 RunGit(['commit', '--amend', '-m',
1440 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001441 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001442 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443
dnjba1b0f32016-09-02 12:37:42 -07001444 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001445 if not self.GitSanityChecks(upstream_branch):
1446 DieWithError('\nGit sanity check failure')
1447
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001448 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001449 if not root:
1450 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001451 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001452
1453 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001454 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001455 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001456 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001457 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001458 except subprocess2.CalledProcessError:
1459 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001460 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001461 'This branch probably doesn\'t exist anymore. To reset the\n'
1462 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001463 ' git branch --set-upstream-to origin/master %s\n'
1464 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001465 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001466
maruel@chromium.org52424302012-08-29 15:14:30 +00001467 issue = self.GetIssue()
1468 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001469 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001470 description = self.GetDescription()
1471 else:
1472 # If the change was never uploaded, use the log messages of all commits
1473 # up to the branch point, as git cl upload will prefill the description
1474 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001475 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1476 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001477
1478 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001479 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001480 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001481 name,
1482 description,
1483 absroot,
1484 files,
1485 issue,
1486 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001487 author,
1488 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001489
dsansomee2d6fd92016-09-08 00:10:47 -07001490 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001491 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001492 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001493 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001494
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001495 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1496 """Sets the description for this CL remotely.
1497
1498 You can get description_lines and footers with GetDescriptionFooters.
1499
1500 Args:
1501 description_lines (list(str)) - List of CL description lines without
1502 newline characters.
1503 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1504 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1505 `List-Of-Tokens`). It will be case-normalized so that each token is
1506 title-cased.
1507 """
1508 new_description = '\n'.join(description_lines)
1509 if footers:
1510 new_description += '\n'
1511 for k, v in footers:
1512 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1513 if not git_footers.FOOTER_PATTERN.match(foot):
1514 raise ValueError('Invalid footer %r' % foot)
1515 new_description += foot + '\n'
1516 self.UpdateDescription(new_description, force)
1517
Edward Lesmes8e282792018-04-03 18:50:29 -04001518 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001519 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1520 try:
1521 return presubmit_support.DoPresubmitChecks(change, committing,
1522 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1523 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001524 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1525 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001526 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001527 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001528
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001529 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1530 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001531 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1532 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001533 else:
1534 # Assume url.
1535 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1536 urlparse.urlparse(issue_arg))
1537 if not parsed_issue_arg or not parsed_issue_arg.valid:
1538 DieWithError('Failed to parse issue argument "%s". '
1539 'Must be an issue number or a valid URL.' % issue_arg)
1540 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001541 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001542
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001543 def CMDUpload(self, options, git_diff_args, orig_args):
1544 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001545 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001546 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001547 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001548 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001549 else:
1550 if self.GetBranch() is None:
1551 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1552
1553 # Default to diffing against common ancestor of upstream branch
1554 base_branch = self.GetCommonAncestorWithUpstream()
1555 git_diff_args = [base_branch, 'HEAD']
1556
Aaron Gablec4c40d12017-05-22 11:49:53 -07001557
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001558 # Fast best-effort checks to abort before running potentially
1559 # expensive hooks if uploading is likely to fail anyway. Passing these
1560 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001561 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001562 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001563
1564 # Apply watchlists on upload.
1565 change = self.GetChange(base_branch, None)
1566 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1567 files = [f.LocalPath() for f in change.AffectedFiles()]
1568 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001569 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001570
1571 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001572 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001573 # Set the reviewer list now so that presubmit checks can access it.
1574 change_description = ChangeDescription(change.FullDescriptionText())
1575 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001576 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001577 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001578 change)
1579 change.SetDescriptionText(change_description.description)
1580 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001581 may_prompt=not options.force,
1582 verbose=options.verbose,
1583 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001584 if not hook_results.should_continue():
1585 return 1
1586 if not options.reviewers and hook_results.reviewers:
1587 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001588 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001589
Aaron Gable13101a62018-02-09 13:20:41 -08001590 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001591 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001592 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001593 _git_set_branch_config_value('last-upload-hash',
1594 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001595 # Run post upload hooks, if specified.
1596 if settings.GetRunPostUploadHook():
1597 presubmit_support.DoPostUploadExecuter(
1598 change,
1599 self,
1600 settings.GetRoot(),
1601 options.verbose,
1602 sys.stdout)
1603
1604 # Upload all dependencies if specified.
1605 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001606 print()
1607 print('--dependencies has been specified.')
1608 print('All dependent local branches will be re-uploaded.')
1609 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001610 # Remove the dependencies flag from args so that we do not end up in a
1611 # loop.
1612 orig_args.remove('--dependencies')
1613 ret = upload_branch_deps(self, orig_args)
1614 return ret
1615
Ravi Mistry31e7d562018-04-02 12:53:57 -04001616 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1617 """Sets labels on the change based on the provided flags.
1618
1619 Sets labels if issue is already uploaded and known, else returns without
1620 doing anything.
1621
1622 Args:
1623 enable_auto_submit: Sets Auto-Submit+1 on the change.
1624 use_commit_queue: Sets Commit-Queue+2 on the change.
1625 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1626 both use_commit_queue and cq_dry_run are true.
1627 """
1628 if not self.GetIssue():
1629 return
1630 try:
1631 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1632 cq_dry_run)
1633 return 0
1634 except KeyboardInterrupt:
1635 raise
1636 except:
1637 labels = []
1638 if enable_auto_submit:
1639 labels.append('Auto-Submit')
1640 if use_commit_queue or cq_dry_run:
1641 labels.append('Commit-Queue')
1642 print('WARNING: Failed to set label(s) on your change: %s\n'
1643 'Either:\n'
1644 ' * Your project does not have the above label(s),\n'
1645 ' * You don\'t have permission to set the above label(s),\n'
1646 ' * There\'s a bug in this code (see stack trace below).\n' %
1647 (', '.join(labels)))
1648 # Still raise exception so that stack trace is printed.
1649 raise
1650
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001651 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001652 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001653
1654 Issue must have been already uploaded and known.
1655 """
1656 assert new_state in _CQState.ALL_STATES
1657 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001658 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001659 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001660 return 0
1661 except KeyboardInterrupt:
1662 raise
1663 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001664 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001665 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001666 ' * Your project has no CQ,\n'
1667 ' * You don\'t have permission to change the CQ state,\n'
1668 ' * There\'s a bug in this code (see stack trace below).\n'
1669 'Consider specifying which bots to trigger manually or asking your '
1670 'project owners for permissions or contacting Chrome Infra at:\n'
1671 'https://www.chromium.org/infra\n\n' %
1672 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001673 # Still raise exception so that stack trace is printed.
1674 raise
1675
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001676 # Forward methods to codereview specific implementation.
1677
Aaron Gable636b13f2017-07-14 10:42:48 -07001678 def AddComment(self, message, publish=None):
1679 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001680
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001681 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001682 """Returns list of _CommentSummary for each comment.
1683
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001684 args:
1685 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001686 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001687 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001688
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001689 def CloseIssue(self):
1690 return self._codereview_impl.CloseIssue()
1691
1692 def GetStatus(self):
1693 return self._codereview_impl.GetStatus()
1694
1695 def GetCodereviewServer(self):
1696 return self._codereview_impl.GetCodereviewServer()
1697
tandriide281ae2016-10-12 06:02:30 -07001698 def GetIssueOwner(self):
1699 """Get owner from codereview, which may differ from this checkout."""
1700 return self._codereview_impl.GetIssueOwner()
1701
Edward Lemur707d70b2018-02-07 00:50:14 +01001702 def GetReviewers(self):
1703 return self._codereview_impl.GetReviewers()
1704
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001705 def GetMostRecentPatchset(self):
1706 return self._codereview_impl.GetMostRecentPatchset()
1707
tandriide281ae2016-10-12 06:02:30 -07001708 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001709 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001710 return self._codereview_impl.CannotTriggerTryJobReason()
1711
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001712 def GetTryJobProperties(self, patchset=None):
1713 """Returns dictionary of properties to launch try job."""
1714 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001715
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001716 def __getattr__(self, attr):
1717 # This is because lots of untested code accesses Rietveld-specific stuff
1718 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001719 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001720 # Note that child method defines __getattr__ as well, and forwards it here,
1721 # because _RietveldChangelistImpl is not cleaned up yet, and given
1722 # deprecation of Rietveld, it should probably be just removed.
1723 # Until that time, avoid infinite recursion by bypassing __getattr__
1724 # of implementation class.
1725 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001726
1727
1728class _ChangelistCodereviewBase(object):
1729 """Abstract base class encapsulating codereview specifics of a changelist."""
1730 def __init__(self, changelist):
1731 self._changelist = changelist # instance of Changelist
1732
1733 def __getattr__(self, attr):
1734 # Forward methods to changelist.
1735 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1736 # _RietveldChangelistImpl to avoid this hack?
1737 return getattr(self._changelist, attr)
1738
1739 def GetStatus(self):
1740 """Apply a rough heuristic to give a simple summary of an issue's review
1741 or CQ status, assuming adherence to a common workflow.
1742
1743 Returns None if no issue for this branch, or specific string keywords.
1744 """
1745 raise NotImplementedError()
1746
1747 def GetCodereviewServer(self):
1748 """Returns server URL without end slash, like "https://codereview.com"."""
1749 raise NotImplementedError()
1750
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001751 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001752 """Fetches and returns description from the codereview server."""
1753 raise NotImplementedError()
1754
tandrii5d48c322016-08-18 16:19:37 -07001755 @classmethod
1756 def IssueConfigKey(cls):
1757 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001758 raise NotImplementedError()
1759
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001760 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001761 def PatchsetConfigKey(cls):
1762 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001763 raise NotImplementedError()
1764
tandrii5d48c322016-08-18 16:19:37 -07001765 @classmethod
1766 def CodereviewServerConfigKey(cls):
1767 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001768 raise NotImplementedError()
1769
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001770 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001771 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001772 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001773
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001774 def GetGerritObjForPresubmit(self):
1775 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1776 return None
1777
dsansomee2d6fd92016-09-08 00:10:47 -07001778 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001779 """Update the description on codereview site."""
1780 raise NotImplementedError()
1781
Aaron Gable636b13f2017-07-14 10:42:48 -07001782 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001783 """Posts a comment to the codereview site."""
1784 raise NotImplementedError()
1785
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001786 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001787 raise NotImplementedError()
1788
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001789 def CloseIssue(self):
1790 """Closes the issue."""
1791 raise NotImplementedError()
1792
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001793 def GetMostRecentPatchset(self):
1794 """Returns the most recent patchset number from the codereview site."""
1795 raise NotImplementedError()
1796
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001797 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001798 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001799 """Fetches and applies the issue.
1800
1801 Arguments:
1802 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1803 reject: if True, reject the failed patch instead of switching to 3-way
1804 merge. Rietveld only.
1805 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1806 only.
1807 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001808 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001809 """
1810 raise NotImplementedError()
1811
1812 @staticmethod
1813 def ParseIssueURL(parsed_url):
1814 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1815 failed."""
1816 raise NotImplementedError()
1817
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001818 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001819 """Best effort check that user is authenticated with codereview server.
1820
1821 Arguments:
1822 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001823 refresh: whether to attempt to refresh credentials. Ignored if not
1824 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001825 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001826 raise NotImplementedError()
1827
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001828 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001829 """Best effort check that uploading isn't supposed to fail for predictable
1830 reasons.
1831
1832 This method should raise informative exception if uploading shouldn't
1833 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001834
1835 Arguments:
1836 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001837 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001838 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001839
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001840 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001841 """Uploads a change to codereview."""
1842 raise NotImplementedError()
1843
Ravi Mistry31e7d562018-04-02 12:53:57 -04001844 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1845 """Sets labels on the change based on the provided flags.
1846
1847 Issue must have been already uploaded and known.
1848 """
1849 raise NotImplementedError()
1850
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001851 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001852 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001853
1854 Issue must have been already uploaded and known.
1855 """
1856 raise NotImplementedError()
1857
tandriie113dfd2016-10-11 10:20:12 -07001858 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001859 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001860 raise NotImplementedError()
1861
tandriide281ae2016-10-12 06:02:30 -07001862 def GetIssueOwner(self):
1863 raise NotImplementedError()
1864
Edward Lemur707d70b2018-02-07 00:50:14 +01001865 def GetReviewers(self):
1866 raise NotImplementedError()
1867
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001868 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001869 raise NotImplementedError()
1870
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001871
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001872class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001873 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001874 # auth_config is Rietveld thing, kept here to preserve interface only.
1875 super(_GerritChangelistImpl, self).__init__(changelist)
1876 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001877 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001878 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001879 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001880 # Map from change number (issue) to its detail cache.
1881 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001882
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001883 if codereview_host is not None:
1884 assert not codereview_host.startswith('https://'), codereview_host
1885 self._gerrit_host = codereview_host
1886 self._gerrit_server = 'https://%s' % codereview_host
1887
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001888 def _GetGerritHost(self):
1889 # Lazy load of configs.
1890 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001891 if self._gerrit_host and '.' not in self._gerrit_host:
1892 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1893 # This happens for internal stuff http://crbug.com/614312.
1894 parsed = urlparse.urlparse(self.GetRemoteUrl())
1895 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001896 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07001897 ' Your current remote is: %s' % self.GetRemoteUrl())
1898 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1899 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001900 return self._gerrit_host
1901
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001902 def _GetGitHost(self):
1903 """Returns git host to be used when uploading change to Gerrit."""
1904 return urlparse.urlparse(self.GetRemoteUrl()).netloc
1905
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001906 def GetCodereviewServer(self):
1907 if not self._gerrit_server:
1908 # If we're on a branch then get the server potentially associated
1909 # with that branch.
1910 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001911 self._gerrit_server = self._GitGetBranchConfigValue(
1912 self.CodereviewServerConfigKey())
1913 if self._gerrit_server:
1914 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001915 if not self._gerrit_server:
1916 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1917 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001918 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001919 parts[0] = parts[0] + '-review'
1920 self._gerrit_host = '.'.join(parts)
1921 self._gerrit_server = 'https://%s' % self._gerrit_host
1922 return self._gerrit_server
1923
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001924 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001925 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001926 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001927 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001928 logging.warn('can\'t detect Gerrit project.')
1929 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001930 project = urlparse.urlparse(remote_url).path.strip('/')
1931 if project.endswith('.git'):
1932 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001933 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1934 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1935 # gitiles/git-over-https protocol. E.g.,
1936 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1937 # as
1938 # https://chromium.googlesource.com/v8/v8
1939 if project.startswith('a/'):
1940 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001941 return project
1942
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001943 def _GerritChangeIdentifier(self):
1944 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1945
1946 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001947 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001948 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001949 project = self._GetGerritProject()
1950 if project:
1951 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1952 # Fall back on still unique, but less efficient change number.
1953 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001954
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001955 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001956 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001957 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001958
tandrii5d48c322016-08-18 16:19:37 -07001959 @classmethod
1960 def PatchsetConfigKey(cls):
1961 return 'gerritpatchset'
1962
1963 @classmethod
1964 def CodereviewServerConfigKey(cls):
1965 return 'gerritserver'
1966
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001967 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001968 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001969 if settings.GetGerritSkipEnsureAuthenticated():
1970 # For projects with unusual authentication schemes.
1971 # See http://crbug.com/603378.
1972 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001973
1974 # Check presence of cookies only if using cookies-based auth method.
1975 cookie_auth = gerrit_util.Authenticator.get()
1976 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001977 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001978
1979 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001980 self.GetCodereviewServer()
1981 git_host = self._GetGitHost()
1982 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001983
1984 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1985 git_auth = cookie_auth.get_auth_header(git_host)
1986 if gerrit_auth and git_auth:
1987 if gerrit_auth == git_auth:
1988 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001989 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001990 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001991 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001992 ' %s\n'
1993 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001994 ' Consider running the following command:\n'
1995 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001996 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02001997 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001998 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001999 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002000 cookie_auth.get_new_password_message(git_host)))
2001 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002002 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002003 return
2004 else:
2005 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002006 ([] if gerrit_auth else [self._gerrit_host]) +
2007 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002008 DieWithError('Credentials for the following hosts are required:\n'
2009 ' %s\n'
2010 'These are read from %s (or legacy %s)\n'
2011 '%s' % (
2012 '\n '.join(missing),
2013 cookie_auth.get_gitcookies_path(),
2014 cookie_auth.get_netrc_path(),
2015 cookie_auth.get_new_password_message(git_host)))
2016
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002017 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002018 if not self.GetIssue():
2019 return
2020
2021 # Warm change details cache now to avoid RPCs later, reducing latency for
2022 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002023 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002024 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002025
2026 status = self._GetChangeDetail()['status']
2027 if status in ('MERGED', 'ABANDONED'):
2028 DieWithError('Change %s has been %s, new uploads are not allowed' %
2029 (self.GetIssueURL(),
2030 'submitted' if status == 'MERGED' else 'abandoned'))
2031
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002032 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2033 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2034 # Apparently this check is not very important? Otherwise get_auth_email
2035 # could have been added to other implementations of Authenticator.
2036 cookies_auth = gerrit_util.Authenticator.get()
2037 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002038 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002039
2040 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002041 if self.GetIssueOwner() == cookies_user:
2042 return
2043 logging.debug('change %s owner is %s, cookies user is %s',
2044 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002045 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002046 # so ask what Gerrit thinks of this user.
2047 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2048 if details['email'] == self.GetIssueOwner():
2049 return
2050 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002051 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002052 'as %s.\n'
2053 'Uploading may fail due to lack of permissions.' %
2054 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2055 confirm_or_exit(action='upload')
2056
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002057 def _PostUnsetIssueProperties(self):
2058 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002059 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002060
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002061 def GetGerritObjForPresubmit(self):
2062 return presubmit_support.GerritAccessor(self._GetGerritHost())
2063
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002064 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002065 """Apply a rough heuristic to give a simple summary of an issue's review
2066 or CQ status, assuming adherence to a common workflow.
2067
2068 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002069 * 'error' - error from review tool (including deleted issues)
2070 * 'unsent' - no reviewers added
2071 * 'waiting' - waiting for review
2072 * 'reply' - waiting for uploader to reply to review
2073 * 'lgtm' - Code-Review label has been set
2074 * 'commit' - in the commit queue
2075 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002076 """
2077 if not self.GetIssue():
2078 return None
2079
2080 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002081 data = self._GetChangeDetail([
2082 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002083 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002084 return 'error'
2085
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002086 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002087 return 'closed'
2088
Aaron Gable9ab38c62017-04-06 14:36:33 -07002089 if data['labels'].get('Commit-Queue', {}).get('approved'):
2090 # The section will have an "approved" subsection if anyone has voted
2091 # the maximum value on the label.
2092 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002093
Aaron Gable9ab38c62017-04-06 14:36:33 -07002094 if data['labels'].get('Code-Review', {}).get('approved'):
2095 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002096
2097 if not data.get('reviewers', {}).get('REVIEWER', []):
2098 return 'unsent'
2099
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002100 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002101 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2102 last_message_author = messages.pop().get('author', {})
2103 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002104 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2105 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002106 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002107 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002108 if last_message_author.get('_account_id') == owner:
2109 # Most recent message was by owner.
2110 return 'waiting'
2111 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002112 # Some reply from non-owner.
2113 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002114
2115 # Somehow there are no messages even though there are reviewers.
2116 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002117
2118 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002119 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002120 patchset = data['revisions'][data['current_revision']]['_number']
2121 self.SetPatchset(patchset)
2122 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002123
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002124 def FetchDescription(self, force=False):
2125 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2126 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002127 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002128 return data['revisions'][current_rev]['commit']['message'].encode(
2129 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002130
dsansomee2d6fd92016-09-08 00:10:47 -07002131 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002132 if gerrit_util.HasPendingChangeEdit(
2133 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002134 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002135 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002136 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002137 'unpublished edit. Either publish the edit in the Gerrit web UI '
2138 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002139
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002140 gerrit_util.DeletePendingChangeEdit(
2141 self._GetGerritHost(), self._GerritChangeIdentifier())
2142 gerrit_util.SetCommitMessage(
2143 self._GetGerritHost(), self._GerritChangeIdentifier(),
2144 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002145
Aaron Gable636b13f2017-07-14 10:42:48 -07002146 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002147 gerrit_util.SetReview(
2148 self._GetGerritHost(), self._GerritChangeIdentifier(),
2149 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002150
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002151 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002152 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002153 messages = self._GetChangeDetail(
2154 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2155 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002156 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002157
2158 # Build dictionary of file comments for easy access and sorting later.
2159 # {author+date: {path: {patchset: {line: url+message}}}}
2160 comments = collections.defaultdict(
2161 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2162 for path, line_comments in file_comments.iteritems():
2163 for comment in line_comments:
2164 if comment.get('tag', '').startswith('autogenerated'):
2165 continue
2166 key = (comment['author']['email'], comment['updated'])
2167 if comment.get('side', 'REVISION') == 'PARENT':
2168 patchset = 'Base'
2169 else:
2170 patchset = 'PS%d' % comment['patch_set']
2171 line = comment.get('line', 0)
2172 url = ('https://%s/c/%s/%s/%s#%s%s' %
2173 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2174 'b' if comment.get('side') == 'PARENT' else '',
2175 str(line) if line else ''))
2176 comments[key][path][patchset][line] = (url, comment['message'])
2177
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002178 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002179 for msg in messages:
2180 # Don't bother showing autogenerated messages.
2181 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2182 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002183 # Gerrit spits out nanoseconds.
2184 assert len(msg['date'].split('.')[-1]) == 9
2185 date = datetime.datetime.strptime(msg['date'][:-3],
2186 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002187 message = msg['message']
2188 key = (msg['author']['email'], msg['date'])
2189 if key in comments:
2190 message += '\n'
2191 for path, patchsets in sorted(comments.get(key, {}).items()):
2192 if readable:
2193 message += '\n%s' % path
2194 for patchset, lines in sorted(patchsets.items()):
2195 for line, (url, content) in sorted(lines.items()):
2196 if line:
2197 line_str = 'Line %d' % line
2198 path_str = '%s:%d:' % (path, line)
2199 else:
2200 line_str = 'File comment'
2201 path_str = '%s:0:' % path
2202 if readable:
2203 message += '\n %s, %s: %s' % (patchset, line_str, url)
2204 message += '\n %s\n' % content
2205 else:
2206 message += '\n%s ' % path_str
2207 message += '\n%s\n' % content
2208
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002209 summary.append(_CommentSummary(
2210 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002211 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002212 sender=msg['author']['email'],
2213 # These could be inferred from the text messages and correlated with
2214 # Code-Review label maximum, however this is not reliable.
2215 # Leaving as is until the need arises.
2216 approval=False,
2217 disapproval=False,
2218 ))
2219 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002220
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002221 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002222 gerrit_util.AbandonChange(
2223 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002224
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002225 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002226 gerrit_util.SubmitChange(
2227 self._GetGerritHost(), self._GerritChangeIdentifier(),
2228 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002229
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002230 def _GetChangeDetail(self, options=None, no_cache=False):
2231 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002232
2233 If fresh data is needed, set no_cache=True which will clear cache and
2234 thus new data will be fetched from Gerrit.
2235 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002236 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002237 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002238
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002239 # Optimization to avoid multiple RPCs:
2240 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2241 'CURRENT_COMMIT' not in options):
2242 options.append('CURRENT_COMMIT')
2243
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002244 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002245 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002246 options = [o.upper() for o in options]
2247
2248 # Check in cache first unless no_cache is True.
2249 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002250 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002251 else:
2252 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002253 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002254 # Assumption: data fetched before with extra options is suitable
2255 # for return for a smaller set of options.
2256 # For example, if we cached data for
2257 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2258 # and request is for options=[CURRENT_REVISION],
2259 # THEN we can return prior cached data.
2260 if options_set.issubset(cached_options_set):
2261 return data
2262
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002263 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002264 data = gerrit_util.GetChangeDetail(
2265 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002266 except gerrit_util.GerritError as e:
2267 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002268 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002269 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002270
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002271 self._detail_cache.setdefault(cache_key, []).append(
2272 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002273 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002274
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002275 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002276 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002277 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002278 data = gerrit_util.GetChangeCommit(
2279 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002280 except gerrit_util.GerritError as e:
2281 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002282 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002283 raise
agable32978d92016-11-01 12:55:02 -07002284 return data
2285
Olivier Robin75ee7252018-04-13 10:02:56 +02002286 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002287 if git_common.is_dirty_git_tree('land'):
2288 return 1
tandriid60367b2016-06-22 05:25:12 -07002289 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2290 if u'Commit-Queue' in detail.get('labels', {}):
2291 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002292 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2293 'which can test and land changes for you. '
2294 'Are you sure you wish to bypass it?\n',
2295 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002296
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002297 differs = True
tandriic4344b52016-08-29 06:04:54 -07002298 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002299 # Note: git diff outputs nothing if there is no diff.
2300 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002301 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002302 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002303 if detail['current_revision'] == last_upload:
2304 differs = False
2305 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002306 print('WARNING: Local branch contents differ from latest uploaded '
2307 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002308 if differs:
2309 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002310 confirm_or_exit(
2311 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2312 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002313 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002314 elif not bypass_hooks:
2315 hook_results = self.RunHook(
2316 committing=True,
2317 may_prompt=not force,
2318 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002319 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2320 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002321 if not hook_results.should_continue():
2322 return 1
2323
2324 self.SubmitIssue(wait_for_merge=True)
2325 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002326 links = self._GetChangeCommit().get('web_links', [])
2327 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002328 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002329 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002330 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002331 return 0
2332
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002333 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002334 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002335 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002336 assert not directory
2337 assert parsed_issue_arg.valid
2338
2339 self._changelist.issue = parsed_issue_arg.issue
2340
2341 if parsed_issue_arg.hostname:
2342 self._gerrit_host = parsed_issue_arg.hostname
2343 self._gerrit_server = 'https://%s' % self._gerrit_host
2344
tandriic2405f52016-10-10 08:13:15 -07002345 try:
2346 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002347 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002348 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002349
2350 if not parsed_issue_arg.patchset:
2351 # Use current revision by default.
2352 revision_info = detail['revisions'][detail['current_revision']]
2353 patchset = int(revision_info['_number'])
2354 else:
2355 patchset = parsed_issue_arg.patchset
2356 for revision_info in detail['revisions'].itervalues():
2357 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2358 break
2359 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002360 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002361 (parsed_issue_arg.patchset, self.GetIssue()))
2362
Aaron Gable697a91b2018-01-19 15:20:15 -08002363 remote_url = self._changelist.GetRemoteUrl()
2364 if remote_url.endswith('.git'):
2365 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002366 remote_url = remote_url.rstrip('/')
2367
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002368 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002369 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002370
2371 if remote_url != fetch_info['url']:
2372 DieWithError('Trying to patch a change from %s but this repo appears '
2373 'to be %s.' % (fetch_info['url'], remote_url))
2374
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002375 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002376
Aaron Gable62619a32017-06-16 08:22:09 -07002377 if force:
2378 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2379 print('Checked out commit for change %i patchset %i locally' %
2380 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002381 elif nocommit:
2382 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2383 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002384 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002385 RunGit(['cherry-pick', 'FETCH_HEAD'])
2386 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002387 (parsed_issue_arg.issue, patchset))
2388 print('Note: this created a local commit which does not have '
2389 'the same hash as the one uploaded for review. This will make '
2390 'uploading changes based on top of this branch difficult.\n'
2391 'If you want to do that, use "git cl patch --force" instead.')
2392
Stefan Zagerd08043c2017-10-12 12:07:02 -07002393 if self.GetBranch():
2394 self.SetIssue(parsed_issue_arg.issue)
2395 self.SetPatchset(patchset)
2396 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2397 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2398 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2399 else:
2400 print('WARNING: You are in detached HEAD state.\n'
2401 'The patch has been applied to your checkout, but you will not be '
2402 'able to upload a new patch set to the gerrit issue.\n'
2403 'Try using the \'-b\' option if you would like to work on a '
2404 'branch and/or upload a new patch set.')
2405
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002406 return 0
2407
2408 @staticmethod
2409 def ParseIssueURL(parsed_url):
2410 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2411 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002412 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2413 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002414 # Short urls like https://domain/<issue_number> can be used, but don't allow
2415 # specifying the patchset (you'd 404), but we allow that here.
2416 if parsed_url.path == '/':
2417 part = parsed_url.fragment
2418 else:
2419 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002420 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002421 if match:
2422 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002423 issue=int(match.group(3)),
2424 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002425 hostname=parsed_url.netloc,
2426 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002427 return None
2428
tandrii16e0b4e2016-06-07 10:34:28 -07002429 def _GerritCommitMsgHookCheck(self, offer_removal):
2430 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2431 if not os.path.exists(hook):
2432 return
2433 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2434 # custom developer made one.
2435 data = gclient_utils.FileRead(hook)
2436 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2437 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002438 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002439 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002440 'and may interfere with it in subtle ways.\n'
2441 'We recommend you remove the commit-msg hook.')
2442 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002443 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002444 gclient_utils.rm_file_or_tree(hook)
2445 print('Gerrit commit-msg hook removed.')
2446 else:
2447 print('OK, will keep Gerrit commit-msg hook in place.')
2448
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002449 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002450 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002451 if options.squash and options.no_squash:
2452 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002453
2454 if not options.squash and not options.no_squash:
2455 # Load default for user, repo, squash=true, in this order.
2456 options.squash = settings.GetSquashGerritUploads()
2457 elif options.no_squash:
2458 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002459
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002460 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002461 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002462
Aaron Gableb56ad332017-01-06 15:24:31 -08002463 # This may be None; default fallback value is determined in logic below.
2464 title = options.title
2465
Dominic Battre7d1c4842017-10-27 09:17:28 +02002466 # Extract bug number from branch name.
2467 bug = options.bug
2468 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2469 if not bug and match:
2470 bug = match.group(1)
2471
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002472 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002473 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002474 if self.GetIssue():
2475 # Try to get the message from a previous upload.
2476 message = self.GetDescription()
2477 if not message:
2478 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002479 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002480 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002481 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002482 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002483 # When uploading a subsequent patchset, -m|--message is taken
2484 # as the patchset title if --title was not provided.
2485 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002486 else:
2487 default_title = RunGit(
2488 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002489 if options.force:
2490 title = default_title
2491 else:
2492 title = ask_for_data(
2493 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002494 change_id = self._GetChangeDetail()['change_id']
2495 while True:
2496 footer_change_ids = git_footers.get_footer_change_id(message)
2497 if footer_change_ids == [change_id]:
2498 break
2499 if not footer_change_ids:
2500 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002501 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002502 continue
2503 # There is already a valid footer but with different or several ids.
2504 # Doing this automatically is non-trivial as we don't want to lose
2505 # existing other footers, yet we want to append just 1 desired
2506 # Change-Id. Thus, just create a new footer, but let user verify the
2507 # new description.
2508 message = '%s\n\nChange-Id: %s' % (message, change_id)
2509 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002510 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002511 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002512 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002513 'Please, check the proposed correction to the description, '
2514 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2515 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2516 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002517 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002518 if not options.force:
2519 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002520 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002521 message = change_desc.description
2522 if not message:
2523 DieWithError("Description is empty. Aborting...")
2524 # Continue the while loop.
2525 # Sanity check of this code - we should end up with proper message
2526 # footer.
2527 assert [change_id] == git_footers.get_footer_change_id(message)
2528 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002529 else: # if not self.GetIssue()
2530 if options.message:
2531 message = options.message
2532 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002533 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002534 if options.title:
2535 message = options.title + '\n\n' + message
2536 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002537
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002538 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002539 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002540 # On first upload, patchset title is always this string, while
2541 # --title flag gets converted to first line of message.
2542 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002543 if not change_desc.description:
2544 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002545 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002546 if len(change_ids) > 1:
2547 DieWithError('too many Change-Id footers, at most 1 allowed.')
2548 if not change_ids:
2549 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002550 change_desc.set_description(git_footers.add_footer_change_id(
2551 change_desc.description,
2552 GenerateGerritChangeId(change_desc.description)))
2553 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002554 assert len(change_ids) == 1
2555 change_id = change_ids[0]
2556
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002557 if options.reviewers or options.tbrs or options.add_owners_to:
2558 change_desc.update_reviewers(options.reviewers, options.tbrs,
2559 options.add_owners_to, change)
2560
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002561 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002562 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2563 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002564 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002565 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2566 desc_tempfile.write(change_desc.description)
2567 desc_tempfile.close()
2568 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2569 '-F', desc_tempfile.name]).strip()
2570 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002571 else:
2572 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002573 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002574 if not change_desc.description:
2575 DieWithError("Description is empty. Aborting...")
2576
2577 if not git_footers.get_footer_change_id(change_desc.description):
2578 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002579 change_desc.set_description(
2580 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002581 if options.reviewers or options.tbrs or options.add_owners_to:
2582 change_desc.update_reviewers(options.reviewers, options.tbrs,
2583 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002584 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002585 # For no-squash mode, we assume the remote called "origin" is the one we
2586 # want. It is not worthwhile to support different workflows for
2587 # no-squash mode.
2588 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002589 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2590
2591 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002592 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002593 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2594 ref_to_push)]).splitlines()
2595 if len(commits) > 1:
2596 print('WARNING: This will upload %d commits. Run the following command '
2597 'to see which commits will be uploaded: ' % len(commits))
2598 print('git log %s..%s' % (parent, ref_to_push))
2599 print('You can also use `git squash-branch` to squash these into a '
2600 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002601 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002602
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002603 if options.reviewers or options.tbrs or options.add_owners_to:
2604 change_desc.update_reviewers(options.reviewers, options.tbrs,
2605 options.add_owners_to, change)
2606
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002607 reviewers = sorted(change_desc.get_reviewers())
2608 # Add cc's from the CC_LIST and --cc flag (if any).
2609 if not options.private and not options.no_autocc:
2610 cc = self.GetCCList().split(',')
2611 else:
2612 cc = []
2613 if options.cc:
2614 cc.extend(options.cc)
2615 cc = filter(None, [email.strip() for email in cc])
2616 if change_desc.get_cced():
2617 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002618 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2619 valid_accounts = set(reviewers + cc)
2620 # TODO(crbug/877717): relax this for all hosts.
2621 else:
2622 valid_accounts = gerrit_util.ValidAccounts(
2623 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002624 logging.info('accounts %s are recognized, %s invalid',
2625 sorted(valid_accounts),
2626 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002627
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002628 # Extra options that can be specified at push time. Doc:
2629 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002630 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002631
Aaron Gable844cf292017-06-28 11:32:59 -07002632 # By default, new changes are started in WIP mode, and subsequent patchsets
2633 # don't send email. At any time, passing --send-mail will mark the change
2634 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002635 if options.send_mail:
2636 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002637 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002638 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002639 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002640 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002641 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002642
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002643 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002644 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002645
Aaron Gable9b713dd2016-12-14 16:04:21 -08002646 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002647 # Punctuation and whitespace in |title| must be percent-encoded.
2648 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002649
agablec6787972016-09-09 16:13:34 -07002650 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002651 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002652
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002653 for r in sorted(reviewers):
2654 if r in valid_accounts:
2655 refspec_opts.append('r=%s' % r)
2656 reviewers.remove(r)
2657 else:
2658 # TODO(tandrii): this should probably be a hard failure.
2659 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2660 % r)
2661 for c in sorted(cc):
2662 # refspec option will be rejected if cc doesn't correspond to an
2663 # account, even though REST call to add such arbitrary cc may succeed.
2664 if c in valid_accounts:
2665 refspec_opts.append('cc=%s' % c)
2666 cc.remove(c)
2667
rmistry9eadede2016-09-19 11:22:43 -07002668 if options.topic:
2669 # Documentation on Gerrit topics is here:
2670 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002671 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002672
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002673 if not change_desc.get_reviewers(tbr_only=True):
2674 # Change is not TBR, so we can inline setting other labels, too.
2675 # TODO(crbug.com/877717): make this working for TBR, too, by figuring out
2676 # max score for CR label somehow.
2677 if options.enable_auto_submit:
2678 refspec_opts.append('l=Auto-Submit+1')
2679 if options.use_commit_queue:
2680 refspec_opts.append('l=Commit-Queue+2')
2681 elif options.cq_dry_run:
2682 refspec_opts.append('l=Commit-Queue+1')
2683
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002684 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002685 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002686 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002687 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002688 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2689
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002690 refspec_suffix = ''
2691 if refspec_opts:
2692 refspec_suffix = '%' + ','.join(refspec_opts)
2693 assert ' ' not in refspec_suffix, (
2694 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2695 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2696
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002697 try:
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002698 before_push = time_time()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002699 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00002700 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemuredcefdc2018-11-08 14:41:42 +00002701 print_stdout=True,
Edward Lemur49c8eaf2018-11-07 22:13:12 +00002702 # Flush after every line: useful for seeing progress when running as
2703 # recipe.
2704 filter_fn=lambda _: sys.stdout.flush())
2705 push_returncode = 0
Edward Lemurfec80c42018-11-01 23:14:14 +00002706 except subprocess2.CalledProcessError as e:
2707 push_returncode = e.returncode
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002708 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002709 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002710 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002711 'credential problems:\n'
2712 ' git cl creds-check\n',
2713 change_desc)
Edward Lemurfec80c42018-11-01 23:14:14 +00002714 finally:
2715 metrics.collector.add_repeated('sub_commands', {
2716 'command': 'git push',
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002717 'execution_time': time_time() - before_push,
Edward Lemurfec80c42018-11-01 23:14:14 +00002718 'exit_code': push_returncode,
2719 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2720 })
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002721
2722 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002723 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002724 change_numbers = [m.group(1)
2725 for m in map(regex.match, push_stdout.splitlines())
2726 if m]
2727 if len(change_numbers) != 1:
2728 DieWithError(
2729 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002730 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002731 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002732 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002733
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002734 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002735 # GetIssue() is not set in case of non-squash uploads according to tests.
2736 # TODO(agable): non-squash uploads in git cl should be removed.
2737 gerrit_util.AddReviewers(
2738 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002739 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002740 reviewers, cc,
2741 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002742
Aaron Gablefd238082017-06-07 13:42:34 -07002743 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09002744 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
2745 score = 1
2746 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
2747 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
2748 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07002749 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002750 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002751 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09002752 msg='Self-approving for TBR',
2753 labels={'Code-Review': score})
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002754 # Labels aren't set through refspec only if tbr is set (see check above).
2755 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
2756 options.cq_dry_run)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002757 return 0
2758
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002759 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2760 change_desc):
2761 """Computes parent of the generated commit to be uploaded to Gerrit.
2762
2763 Returns revision or a ref name.
2764 """
2765 if custom_cl_base:
2766 # Try to avoid creating additional unintended CLs when uploading, unless
2767 # user wants to take this risk.
2768 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2769 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2770 local_ref_of_target_remote])
2771 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002772 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002773 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2774 'If you proceed with upload, more than 1 CL may be created by '
2775 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2776 'If you are certain that specified base `%s` has already been '
2777 'uploaded to Gerrit as another CL, you may proceed.\n' %
2778 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2779 if not force:
2780 confirm_or_exit(
2781 'Do you take responsibility for cleaning up potential mess '
2782 'resulting from proceeding with upload?',
2783 action='upload')
2784 return custom_cl_base
2785
Aaron Gablef97e33d2017-03-30 15:44:27 -07002786 if remote != '.':
2787 return self.GetCommonAncestorWithUpstream()
2788
2789 # If our upstream branch is local, we base our squashed commit on its
2790 # squashed version.
2791 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2792
Aaron Gablef97e33d2017-03-30 15:44:27 -07002793 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002794 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002795
2796 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002797 # TODO(tandrii): consider checking parent change in Gerrit and using its
2798 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2799 # the tree hash of the parent branch. The upside is less likely bogus
2800 # requests to reupload parent change just because it's uploadhash is
2801 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002802 parent = RunGit(['config',
2803 'branch.%s.gerritsquashhash' % upstream_branch_name],
2804 error_ok=True).strip()
2805 # Verify that the upstream branch has been uploaded too, otherwise
2806 # Gerrit will create additional CLs when uploading.
2807 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2808 RunGitSilent(['rev-parse', parent + ':'])):
2809 DieWithError(
2810 '\nUpload upstream branch %s first.\n'
2811 'It is likely that this branch has been rebased since its last '
2812 'upload, so you just need to upload it again.\n'
2813 '(If you uploaded it with --no-squash, then branch dependencies '
2814 'are not supported, and you should reupload with --squash.)'
2815 % upstream_branch_name,
2816 change_desc)
2817 return parent
2818
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002819 def _AddChangeIdToCommitMessage(self, options, args):
2820 """Re-commits using the current message, assumes the commit hook is in
2821 place.
2822 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002823 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002824 git_command = ['commit', '--amend', '-m', log_desc]
2825 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002826 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002827 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002828 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002829 return new_log_desc
2830 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002831 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002832
Ravi Mistry31e7d562018-04-02 12:53:57 -04002833 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2834 """Sets labels on the change based on the provided flags."""
2835 labels = {}
2836 notify = None;
2837 if enable_auto_submit:
2838 labels['Auto-Submit'] = 1
2839 if use_commit_queue:
2840 labels['Commit-Queue'] = 2
2841 elif cq_dry_run:
2842 labels['Commit-Queue'] = 1
2843 notify = False
2844 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002845 gerrit_util.SetReview(
2846 self._GetGerritHost(),
2847 self._GerritChangeIdentifier(),
2848 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04002849
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002850 def SetCQState(self, new_state):
2851 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002852 vote_map = {
2853 _CQState.NONE: 0,
2854 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002855 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002856 }
Aaron Gablefc62f762017-07-17 11:12:07 -07002857 labels = {'Commit-Queue': vote_map[new_state]}
2858 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002859 gerrit_util.SetReview(
2860 self._GetGerritHost(), self._GerritChangeIdentifier(),
2861 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002862
tandriie113dfd2016-10-11 10:20:12 -07002863 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002864 try:
2865 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002866 except GerritChangeNotExists:
2867 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002868
2869 if data['status'] in ('ABANDONED', 'MERGED'):
2870 return 'CL %s is closed' % self.GetIssue()
2871
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002872 def GetTryJobProperties(self, patchset=None):
2873 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002874 data = self._GetChangeDetail(['ALL_REVISIONS'])
2875 patchset = int(patchset or self.GetPatchset())
2876 assert patchset
2877 revision_data = None # Pylint wants it to be defined.
2878 for revision_data in data['revisions'].itervalues():
2879 if int(revision_data['_number']) == patchset:
2880 break
2881 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002882 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002883 (patchset, self.GetIssue()))
2884 return {
2885 'patch_issue': self.GetIssue(),
2886 'patch_set': patchset or self.GetPatchset(),
2887 'patch_project': data['project'],
2888 'patch_storage': 'gerrit',
2889 'patch_ref': revision_data['fetch']['http']['ref'],
2890 'patch_repository_url': revision_data['fetch']['http']['url'],
2891 'patch_gerrit_url': self.GetCodereviewServer(),
2892 }
tandriie113dfd2016-10-11 10:20:12 -07002893
tandriide281ae2016-10-12 06:02:30 -07002894 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002895 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002896
Edward Lemur707d70b2018-02-07 00:50:14 +01002897 def GetReviewers(self):
2898 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002899 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002900
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002901
2902_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002903 'gerrit': _GerritChangelistImpl,
2904}
2905
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002906
iannuccie53c9352016-08-17 14:40:40 -07002907def _add_codereview_issue_select_options(parser, extra=""):
2908 _add_codereview_select_options(parser)
2909
2910 text = ('Operate on this issue number instead of the current branch\'s '
2911 'implicit issue.')
2912 if extra:
2913 text += ' '+extra
2914 parser.add_option('-i', '--issue', type=int, help=text)
2915
2916
2917def _process_codereview_issue_select_options(parser, options):
2918 _process_codereview_select_options(parser, options)
2919 if options.issue is not None and not options.forced_codereview:
2920 parser.error('--issue must be specified with either --rietveld or --gerrit')
2921
2922
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002923def _add_codereview_select_options(parser):
2924 """Appends --gerrit and --rietveld options to force specific codereview."""
2925 parser.codereview_group = optparse.OptionGroup(
2926 parser, 'EXPERIMENTAL! Codereview override options')
2927 parser.add_option_group(parser.codereview_group)
2928 parser.codereview_group.add_option(
2929 '--gerrit', action='store_true',
2930 help='Force the use of Gerrit for codereview')
2931 parser.codereview_group.add_option(
2932 '--rietveld', action='store_true',
2933 help='Force the use of Rietveld for codereview')
2934
2935
2936def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00002937 if options.rietveld:
2938 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002939 options.forced_codereview = None
2940 if options.gerrit:
2941 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002942
2943
tandriif9aefb72016-07-01 09:06:51 -07002944def _get_bug_line_values(default_project, bugs):
2945 """Given default_project and comma separated list of bugs, yields bug line
2946 values.
2947
2948 Each bug can be either:
2949 * a number, which is combined with default_project
2950 * string, which is left as is.
2951
2952 This function may produce more than one line, because bugdroid expects one
2953 project per line.
2954
2955 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2956 ['v8:123', 'chromium:789']
2957 """
2958 default_bugs = []
2959 others = []
2960 for bug in bugs.split(','):
2961 bug = bug.strip()
2962 if bug:
2963 try:
2964 default_bugs.append(int(bug))
2965 except ValueError:
2966 others.append(bug)
2967
2968 if default_bugs:
2969 default_bugs = ','.join(map(str, default_bugs))
2970 if default_project:
2971 yield '%s:%s' % (default_project, default_bugs)
2972 else:
2973 yield default_bugs
2974 for other in sorted(others):
2975 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2976 yield other
2977
2978
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002979class ChangeDescription(object):
2980 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002981 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002982 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002983 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002984 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002985 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2986 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
2987 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
2988 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002989
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002990 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002991 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002992
agable@chromium.org42c20792013-09-12 17:34:49 +00002993 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002994 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002995 return '\n'.join(self._description_lines)
2996
2997 def set_description(self, desc):
2998 if isinstance(desc, basestring):
2999 lines = desc.splitlines()
3000 else:
3001 lines = [line.rstrip() for line in desc]
3002 while lines and not lines[0]:
3003 lines.pop(0)
3004 while lines and not lines[-1]:
3005 lines.pop(-1)
3006 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003007
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003008 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3009 """Rewrites the R=/TBR= line(s) as a single line each.
3010
3011 Args:
3012 reviewers (list(str)) - list of additional emails to use for reviewers.
3013 tbrs (list(str)) - list of additional emails to use for TBRs.
3014 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3015 the change that are missing OWNER coverage. If this is not None, you
3016 must also pass a value for `change`.
3017 change (Change) - The Change that should be used for OWNERS lookups.
3018 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003019 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003020 assert isinstance(tbrs, list), tbrs
3021
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003022 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003023 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003024
3025 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003026 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003027
3028 reviewers = set(reviewers)
3029 tbrs = set(tbrs)
3030 LOOKUP = {
3031 'TBR': tbrs,
3032 'R': reviewers,
3033 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003034
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003035 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003036 regexp = re.compile(self.R_LINE)
3037 matches = [regexp.match(line) for line in self._description_lines]
3038 new_desc = [l for i, l in enumerate(self._description_lines)
3039 if not matches[i]]
3040 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003041
agable@chromium.org42c20792013-09-12 17:34:49 +00003042 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003043
3044 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003045 for match in matches:
3046 if not match:
3047 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003048 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3049
3050 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003051 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003052 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003053 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003054 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003055 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003056 LOOKUP[add_owners_to].update(
3057 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003058
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003059 # If any folks ended up in both groups, remove them from tbrs.
3060 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003061
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003062 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3063 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003064
3065 # Put the new lines in the description where the old first R= line was.
3066 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3067 if 0 <= line_loc < len(self._description_lines):
3068 if new_tbr_line:
3069 self._description_lines.insert(line_loc, new_tbr_line)
3070 if new_r_line:
3071 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003072 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003073 if new_r_line:
3074 self.append_footer(new_r_line)
3075 if new_tbr_line:
3076 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003077
Aaron Gable3a16ed12017-03-23 10:51:55 -07003078 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003079 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003080 self.set_description([
3081 '# Enter a description of the change.',
3082 '# This will be displayed on the codereview site.',
3083 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003084 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003085 '--------------------',
3086 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003087
agable@chromium.org42c20792013-09-12 17:34:49 +00003088 regexp = re.compile(self.BUG_LINE)
3089 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003090 prefix = settings.GetBugPrefix()
3091 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003092 if git_footer:
3093 self.append_footer('Bug: %s' % ', '.join(values))
3094 else:
3095 for value in values:
3096 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003097
agable@chromium.org42c20792013-09-12 17:34:49 +00003098 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003099 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003100 if not content:
3101 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003102 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003103
Bruce Dawson2377b012018-01-11 16:46:49 -08003104 # Strip off comments and default inserted "Bug:" line.
3105 clean_lines = [line.rstrip() for line in lines if not
3106 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003107 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003108 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003109 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003110
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003111 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003112 """Adds a footer line to the description.
3113
3114 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3115 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3116 that Gerrit footers are always at the end.
3117 """
3118 parsed_footer_line = git_footers.parse_footer(line)
3119 if parsed_footer_line:
3120 # Line is a gerrit footer in the form: Footer-Key: any value.
3121 # Thus, must be appended observing Gerrit footer rules.
3122 self.set_description(
3123 git_footers.add_footer(self.description,
3124 key=parsed_footer_line[0],
3125 value=parsed_footer_line[1]))
3126 return
3127
3128 if not self._description_lines:
3129 self._description_lines.append(line)
3130 return
3131
3132 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3133 if gerrit_footers:
3134 # git_footers.split_footers ensures that there is an empty line before
3135 # actual (gerrit) footers, if any. We have to keep it that way.
3136 assert top_lines and top_lines[-1] == ''
3137 top_lines, separator = top_lines[:-1], top_lines[-1:]
3138 else:
3139 separator = [] # No need for separator if there are no gerrit_footers.
3140
3141 prev_line = top_lines[-1] if top_lines else ''
3142 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3143 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3144 top_lines.append('')
3145 top_lines.append(line)
3146 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003147
tandrii99a72f22016-08-17 14:33:24 -07003148 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003149 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003150 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003151 reviewers = [match.group(2).strip()
3152 for match in matches
3153 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003154 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003155
bradnelsond975b302016-10-23 12:20:23 -07003156 def get_cced(self):
3157 """Retrieves the list of reviewers."""
3158 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3159 cced = [match.group(2).strip() for match in matches if match]
3160 return cleanup_list(cced)
3161
Nodir Turakulov23b82142017-11-16 11:04:25 -08003162 def get_hash_tags(self):
3163 """Extracts and sanitizes a list of Gerrit hashtags."""
3164 subject = (self._description_lines or ('',))[0]
3165 subject = re.sub(
3166 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3167
3168 tags = []
3169 start = 0
3170 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3171 while True:
3172 m = bracket_exp.match(subject, start)
3173 if not m:
3174 break
3175 tags.append(self.sanitize_hash_tag(m.group(1)))
3176 start = m.end()
3177
3178 if not tags:
3179 # Try "Tag: " prefix.
3180 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3181 if m:
3182 tags.append(self.sanitize_hash_tag(m.group(1)))
3183 return tags
3184
3185 @classmethod
3186 def sanitize_hash_tag(cls, tag):
3187 """Returns a sanitized Gerrit hash tag.
3188
3189 A sanitized hashtag can be used as a git push refspec parameter value.
3190 """
3191 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3192
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003193 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3194 """Updates this commit description given the parent.
3195
3196 This is essentially what Gnumbd used to do.
3197 Consult https://goo.gl/WMmpDe for more details.
3198 """
3199 assert parent_msg # No, orphan branch creation isn't supported.
3200 assert parent_hash
3201 assert dest_ref
3202 parent_footer_map = git_footers.parse_footers(parent_msg)
3203 # This will also happily parse svn-position, which GnumbD is no longer
3204 # supporting. While we'd generate correct footers, the verifier plugin
3205 # installed in Gerrit will block such commit (ie git push below will fail).
3206 parent_position = git_footers.get_position(parent_footer_map)
3207
3208 # Cherry-picks may have last line obscuring their prior footers,
3209 # from git_footers perspective. This is also what Gnumbd did.
3210 cp_line = None
3211 if (self._description_lines and
3212 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3213 cp_line = self._description_lines.pop()
3214
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003215 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003216
3217 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3218 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003219 for i, line in enumerate(footer_lines):
3220 k, v = git_footers.parse_footer(line) or (None, None)
3221 if k and k.startswith('Cr-'):
3222 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003223
3224 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003225 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003226 if parent_position[0] == dest_ref:
3227 # Same branch as parent.
3228 number = int(parent_position[1]) + 1
3229 else:
3230 number = 1 # New branch, and extra lineage.
3231 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3232 int(parent_position[1])))
3233
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003234 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3235 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003236
3237 self._description_lines = top_lines
3238 if cp_line:
3239 self._description_lines.append(cp_line)
3240 if self._description_lines[-1] != '':
3241 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003242 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003243
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003244
Aaron Gablea1bab272017-04-11 16:38:18 -07003245def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003246 """Retrieves the reviewers that approved a CL from the issue properties with
3247 messages.
3248
3249 Note that the list may contain reviewers that are not committer, thus are not
3250 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003251
3252 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003253 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003254 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003255 return sorted(
3256 set(
3257 message['sender']
3258 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003259 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003260 )
3261 )
3262
3263
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003264def FindCodereviewSettingsFile(filename='codereview.settings'):
3265 """Finds the given file starting in the cwd and going up.
3266
3267 Only looks up to the top of the repository unless an
3268 'inherit-review-settings-ok' file exists in the root of the repository.
3269 """
3270 inherit_ok_file = 'inherit-review-settings-ok'
3271 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003272 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003273 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3274 root = '/'
3275 while True:
3276 if filename in os.listdir(cwd):
3277 if os.path.isfile(os.path.join(cwd, filename)):
3278 return open(os.path.join(cwd, filename))
3279 if cwd == root:
3280 break
3281 cwd = os.path.dirname(cwd)
3282
3283
3284def LoadCodereviewSettingsFromFile(fileobj):
3285 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003286 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003288 def SetProperty(name, setting, unset_error_ok=False):
3289 fullname = 'rietveld.' + name
3290 if setting in keyvals:
3291 RunGit(['config', fullname, keyvals[setting]])
3292 else:
3293 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3294
tandrii48df5812016-10-17 03:55:37 -07003295 if not keyvals.get('GERRIT_HOST', False):
3296 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003297 # Only server setting is required. Other settings can be absent.
3298 # In that case, we ignore errors raised during option deletion attempt.
3299 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3300 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3301 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003302 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003303 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3304 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003305 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3306 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003307
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003308 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003309 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003310
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003311 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003312 RunGit(['config', 'gerrit.squash-uploads',
3313 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003314
tandrii@chromium.org28253532016-04-14 13:46:56 +00003315 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003316 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003317 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3318
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003319 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003320 # should be of the form
3321 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3322 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003323 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3324 keyvals['ORIGIN_URL_CONFIG']])
3325
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003326
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003327def urlretrieve(source, destination):
3328 """urllib is broken for SSL connections via a proxy therefore we
3329 can't use urllib.urlretrieve()."""
3330 with open(destination, 'w') as f:
3331 f.write(urllib2.urlopen(source).read())
3332
3333
ukai@chromium.org712d6102013-11-27 00:52:58 +00003334def hasSheBang(fname):
3335 """Checks fname is a #! script."""
3336 with open(fname) as f:
3337 return f.read(2).startswith('#!')
3338
3339
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003340# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3341def DownloadHooks(*args, **kwargs):
3342 pass
3343
3344
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003345def DownloadGerritHook(force):
3346 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003347
3348 Args:
3349 force: True to update hooks. False to install hooks if not present.
3350 """
3351 if not settings.GetIsGerrit():
3352 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003353 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003354 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3355 if not os.access(dst, os.X_OK):
3356 if os.path.exists(dst):
3357 if not force:
3358 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003359 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003360 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003361 if not hasSheBang(dst):
3362 DieWithError('Not a script: %s\n'
3363 'You need to download from\n%s\n'
3364 'into .git/hooks/commit-msg and '
3365 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003366 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3367 except Exception:
3368 if os.path.exists(dst):
3369 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003370 DieWithError('\nFailed to download hooks.\n'
3371 'You need to download from\n%s\n'
3372 'into .git/hooks/commit-msg and '
3373 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003374
3375
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003376class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003377 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003378
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003379 _GOOGLESOURCE = 'googlesource.com'
3380
3381 def __init__(self):
3382 # Cached list of [host, identity, source], where source is either
3383 # .gitcookies or .netrc.
3384 self._all_hosts = None
3385
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003386 def ensure_configured_gitcookies(self):
3387 """Runs checks and suggests fixes to make git use .gitcookies from default
3388 path."""
3389 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3390 configured_path = RunGitSilent(
3391 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003392 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003393 if configured_path:
3394 self._ensure_default_gitcookies_path(configured_path, default)
3395 else:
3396 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003397
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003398 @staticmethod
3399 def _ensure_default_gitcookies_path(configured_path, default_path):
3400 assert configured_path
3401 if configured_path == default_path:
3402 print('git is already configured to use your .gitcookies from %s' %
3403 configured_path)
3404 return
3405
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003406 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003407 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3408 (configured_path, default_path))
3409
3410 if not os.path.exists(configured_path):
3411 print('However, your configured .gitcookies file is missing.')
3412 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3413 action='reconfigure')
3414 RunGit(['config', '--global', 'http.cookiefile', default_path])
3415 return
3416
3417 if os.path.exists(default_path):
3418 print('WARNING: default .gitcookies file already exists %s' %
3419 default_path)
3420 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3421 default_path)
3422
3423 confirm_or_exit('Move existing .gitcookies to default location?',
3424 action='move')
3425 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003426 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003427 print('Moved and reconfigured git to use .gitcookies from %s' %
3428 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003429
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003430 @staticmethod
3431 def _configure_gitcookies_path(default_path):
3432 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3433 if os.path.exists(netrc_path):
3434 print('You seem to be using outdated .netrc for git credentials: %s' %
3435 netrc_path)
3436 print('This tool will guide you through setting up recommended '
3437 '.gitcookies store for git credentials.\n'
3438 '\n'
3439 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3440 ' git config --global --unset http.cookiefile\n'
3441 ' mv %s %s.backup\n\n' % (default_path, default_path))
3442 confirm_or_exit(action='setup .gitcookies')
3443 RunGit(['config', '--global', 'http.cookiefile', default_path])
3444 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003445
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003446 def get_hosts_with_creds(self, include_netrc=False):
3447 if self._all_hosts is None:
3448 a = gerrit_util.CookiesAuthenticator()
3449 self._all_hosts = [
3450 (h, u, s)
3451 for h, u, s in itertools.chain(
3452 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3453 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3454 )
3455 if h.endswith(self._GOOGLESOURCE)
3456 ]
3457
3458 if include_netrc:
3459 return self._all_hosts
3460 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3461
3462 def print_current_creds(self, include_netrc=False):
3463 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3464 if not hosts:
3465 print('No Git/Gerrit credentials found')
3466 return
3467 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3468 header = [('Host', 'User', 'Which file'),
3469 ['=' * l for l in lengths]]
3470 for row in (header + hosts):
3471 print('\t'.join((('%%+%ds' % l) % s)
3472 for l, s in zip(lengths, row)))
3473
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003474 @staticmethod
3475 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003476 """Parses identity "git-<username>.domain" into <username> and domain."""
3477 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003478 # distinguishable from sub-domains. But we do know typical domains:
3479 if identity.endswith('.chromium.org'):
3480 domain = 'chromium.org'
3481 username = identity[:-len('.chromium.org')]
3482 else:
3483 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003484 if username.startswith('git-'):
3485 username = username[len('git-'):]
3486 return username, domain
3487
3488 def _get_usernames_of_domain(self, domain):
3489 """Returns list of usernames referenced by .gitcookies in a given domain."""
3490 identities_by_domain = {}
3491 for _, identity, _ in self.get_hosts_with_creds():
3492 username, domain = self._parse_identity(identity)
3493 identities_by_domain.setdefault(domain, []).append(username)
3494 return identities_by_domain.get(domain)
3495
3496 def _canonical_git_googlesource_host(self, host):
3497 """Normalizes Gerrit hosts (with '-review') to Git host."""
3498 assert host.endswith(self._GOOGLESOURCE)
3499 # Prefix doesn't include '.' at the end.
3500 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3501 if prefix.endswith('-review'):
3502 prefix = prefix[:-len('-review')]
3503 return prefix + '.' + self._GOOGLESOURCE
3504
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003505 def _canonical_gerrit_googlesource_host(self, host):
3506 git_host = self._canonical_git_googlesource_host(host)
3507 prefix = git_host.split('.', 1)[0]
3508 return prefix + '-review.' + self._GOOGLESOURCE
3509
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003510 def _get_counterpart_host(self, host):
3511 assert host.endswith(self._GOOGLESOURCE)
3512 git = self._canonical_git_googlesource_host(host)
3513 gerrit = self._canonical_gerrit_googlesource_host(git)
3514 return git if gerrit == host else gerrit
3515
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003516 def has_generic_host(self):
3517 """Returns whether generic .googlesource.com has been configured.
3518
3519 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3520 """
3521 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3522 if host == '.' + self._GOOGLESOURCE:
3523 return True
3524 return False
3525
3526 def _get_git_gerrit_identity_pairs(self):
3527 """Returns map from canonic host to pair of identities (Git, Gerrit).
3528
3529 One of identities might be None, meaning not configured.
3530 """
3531 host_to_identity_pairs = {}
3532 for host, identity, _ in self.get_hosts_with_creds():
3533 canonical = self._canonical_git_googlesource_host(host)
3534 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3535 idx = 0 if canonical == host else 1
3536 pair[idx] = identity
3537 return host_to_identity_pairs
3538
3539 def get_partially_configured_hosts(self):
3540 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003541 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3542 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3543 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003544
3545 def get_conflicting_hosts(self):
3546 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003547 host
3548 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003549 if None not in (i1, i2) and i1 != i2)
3550
3551 def get_duplicated_hosts(self):
3552 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3553 return set(host for host, count in counters.iteritems() if count > 1)
3554
3555 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3556 'chromium.googlesource.com': 'chromium.org',
3557 'chrome-internal.googlesource.com': 'google.com',
3558 }
3559
3560 def get_hosts_with_wrong_identities(self):
3561 """Finds hosts which **likely** reference wrong identities.
3562
3563 Note: skips hosts which have conflicting identities for Git and Gerrit.
3564 """
3565 hosts = set()
3566 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3567 pair = self._get_git_gerrit_identity_pairs().get(host)
3568 if pair and pair[0] == pair[1]:
3569 _, domain = self._parse_identity(pair[0])
3570 if domain != expected:
3571 hosts.add(host)
3572 return hosts
3573
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003574 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003575 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003576 hosts = sorted(hosts)
3577 assert hosts
3578 if extra_column_func is None:
3579 extras = [''] * len(hosts)
3580 else:
3581 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003582 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3583 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003584 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003585 lines.append(tmpl % he)
3586 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003587
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003588 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003589 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003590 yield ('.googlesource.com wildcard record detected',
3591 ['Chrome Infrastructure team recommends to list full host names '
3592 'explicitly.'],
3593 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003594
3595 dups = self.get_duplicated_hosts()
3596 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003597 yield ('The following hosts were defined twice',
3598 self._format_hosts(dups),
3599 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003600
3601 partial = self.get_partially_configured_hosts()
3602 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003603 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3604 'These hosts are missing',
3605 self._format_hosts(partial, lambda host: 'but %s defined' %
3606 self._get_counterpart_host(host)),
3607 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003608
3609 conflicting = self.get_conflicting_hosts()
3610 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003611 yield ('The following Git hosts have differing credentials from their '
3612 'Gerrit counterparts',
3613 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3614 tuple(self._get_git_gerrit_identity_pairs()[host])),
3615 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003616
3617 wrong = self.get_hosts_with_wrong_identities()
3618 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003619 yield ('These hosts likely use wrong identity',
3620 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3621 (self._get_git_gerrit_identity_pairs()[host][0],
3622 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3623 wrong)
3624
3625 def find_and_report_problems(self):
3626 """Returns True if there was at least one problem, else False."""
3627 found = False
3628 bad_hosts = set()
3629 for title, sublines, hosts in self._find_problems():
3630 if not found:
3631 found = True
3632 print('\n\n.gitcookies problem report:\n')
3633 bad_hosts.update(hosts or [])
3634 print(' %s%s' % (title , (':' if sublines else '')))
3635 if sublines:
3636 print()
3637 print(' %s' % '\n '.join(sublines))
3638 print()
3639
3640 if bad_hosts:
3641 assert found
3642 print(' You can manually remove corresponding lines in your %s file and '
3643 'visit the following URLs with correct account to generate '
3644 'correct credential lines:\n' %
3645 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3646 print(' %s' % '\n '.join(sorted(set(
3647 gerrit_util.CookiesAuthenticator().get_new_password_url(
3648 self._canonical_git_googlesource_host(host))
3649 for host in bad_hosts
3650 ))))
3651 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003652
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003653
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003654@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003655def CMDcreds_check(parser, args):
3656 """Checks credentials and suggests changes."""
3657 _, _ = parser.parse_args(args)
3658
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003659 # Code below checks .gitcookies. Abort if using something else.
3660 authn = gerrit_util.Authenticator.get()
3661 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3662 if isinstance(authn, gerrit_util.GceAuthenticator):
3663 DieWithError(
3664 'This command is not designed for GCE, are you on a bot?\n'
3665 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3666 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003667 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003668 'This command is not designed for bot environment. It checks '
3669 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003670
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003671 checker = _GitCookiesChecker()
3672 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003673
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003674 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003675 checker.print_current_creds(include_netrc=True)
3676
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003677 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003678 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003679 return 0
3680 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003681
3682
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003683@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003684def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003685 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003686 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3687 branch = ShortBranchName(branchref)
3688 _, args = parser.parse_args(args)
3689 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003690 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003691 return RunGit(['config', 'branch.%s.base-url' % branch],
3692 error_ok=False).strip()
3693 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003694 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003695 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3696 error_ok=False).strip()
3697
3698
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003699def color_for_status(status):
3700 """Maps a Changelist status to color, for CMDstatus and other tools."""
3701 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003702 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003703 'waiting': Fore.BLUE,
3704 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003705 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003706 'lgtm': Fore.GREEN,
3707 'commit': Fore.MAGENTA,
3708 'closed': Fore.CYAN,
3709 'error': Fore.WHITE,
3710 }.get(status, Fore.WHITE)
3711
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003712
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003713def get_cl_statuses(changes, fine_grained, max_processes=None):
3714 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003715
3716 If fine_grained is true, this will fetch CL statuses from the server.
3717 Otherwise, simply indicate if there's a matching url for the given branches.
3718
3719 If max_processes is specified, it is used as the maximum number of processes
3720 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3721 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003722
3723 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003724 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003725 if not changes:
3726 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003727
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003728 if not fine_grained:
3729 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003730 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003731 for cl in changes:
3732 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003733 return
3734
3735 # First, sort out authentication issues.
3736 logging.debug('ensuring credentials exist')
3737 for cl in changes:
3738 cl.EnsureAuthenticated(force=False, refresh=True)
3739
3740 def fetch(cl):
3741 try:
3742 return (cl, cl.GetStatus())
3743 except:
3744 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003745 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003746 raise
3747
3748 threads_count = len(changes)
3749 if max_processes:
3750 threads_count = max(1, min(threads_count, max_processes))
3751 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3752
3753 pool = ThreadPool(threads_count)
3754 fetched_cls = set()
3755 try:
3756 it = pool.imap_unordered(fetch, changes).__iter__()
3757 while True:
3758 try:
3759 cl, status = it.next(timeout=5)
3760 except multiprocessing.TimeoutError:
3761 break
3762 fetched_cls.add(cl)
3763 yield cl, status
3764 finally:
3765 pool.close()
3766
3767 # Add any branches that failed to fetch.
3768 for cl in set(changes) - fetched_cls:
3769 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003770
rmistry@google.com2dd99862015-06-22 12:22:18 +00003771
3772def upload_branch_deps(cl, args):
3773 """Uploads CLs of local branches that are dependents of the current branch.
3774
3775 If the local branch dependency tree looks like:
3776 test1 -> test2.1 -> test3.1
3777 -> test3.2
3778 -> test2.2 -> test3.3
3779
3780 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3781 run on the dependent branches in this order:
3782 test2.1, test3.1, test3.2, test2.2, test3.3
3783
3784 Note: This function does not rebase your local dependent branches. Use it when
3785 you make a change to the parent branch that will not conflict with its
3786 dependent branches, and you would like their dependencies updated in
3787 Rietveld.
3788 """
3789 if git_common.is_dirty_git_tree('upload-branch-deps'):
3790 return 1
3791
3792 root_branch = cl.GetBranch()
3793 if root_branch is None:
3794 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3795 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003796 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003797 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3798 'patchset dependencies without an uploaded CL.')
3799
3800 branches = RunGit(['for-each-ref',
3801 '--format=%(refname:short) %(upstream:short)',
3802 'refs/heads'])
3803 if not branches:
3804 print('No local branches found.')
3805 return 0
3806
3807 # Create a dictionary of all local branches to the branches that are dependent
3808 # on it.
3809 tracked_to_dependents = collections.defaultdict(list)
3810 for b in branches.splitlines():
3811 tokens = b.split()
3812 if len(tokens) == 2:
3813 branch_name, tracked = tokens
3814 tracked_to_dependents[tracked].append(branch_name)
3815
vapiera7fbd5a2016-06-16 09:17:49 -07003816 print()
3817 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003818 dependents = []
3819 def traverse_dependents_preorder(branch, padding=''):
3820 dependents_to_process = tracked_to_dependents.get(branch, [])
3821 padding += ' '
3822 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003823 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003824 dependents.append(dependent)
3825 traverse_dependents_preorder(dependent, padding)
3826 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003827 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003828
3829 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003830 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003831 return 0
3832
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003833 confirm_or_exit('This command will checkout all dependent branches and run '
3834 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003835
rmistry@google.com2dd99862015-06-22 12:22:18 +00003836 # Record all dependents that failed to upload.
3837 failures = {}
3838 # Go through all dependents, checkout the branch and upload.
3839 try:
3840 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003841 print()
3842 print('--------------------------------------')
3843 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003844 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003845 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003846 try:
3847 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003848 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003849 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003850 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003851 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003852 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003853 finally:
3854 # Swap back to the original root branch.
3855 RunGit(['checkout', '-q', root_branch])
3856
vapiera7fbd5a2016-06-16 09:17:49 -07003857 print()
3858 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003859 for dependent_branch in dependents:
3860 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003861 print(' %s : %s' % (dependent_branch, upload_status))
3862 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003863
3864 return 0
3865
3866
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003867@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003868def CMDarchive(parser, args):
3869 """Archives and deletes branches associated with closed changelists."""
3870 parser.add_option(
3871 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003872 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003873 parser.add_option(
3874 '-f', '--force', action='store_true',
3875 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003876 parser.add_option(
3877 '-d', '--dry-run', action='store_true',
3878 help='Skip the branch tagging and removal steps.')
3879 parser.add_option(
3880 '-t', '--notags', action='store_true',
3881 help='Do not tag archived branches. '
3882 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003883
3884 auth.add_auth_options(parser)
3885 options, args = parser.parse_args(args)
3886 if args:
3887 parser.error('Unsupported args: %s' % ' '.join(args))
3888 auth_config = auth.extract_auth_config_from_options(options)
3889
3890 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3891 if not branches:
3892 return 0
3893
vapiera7fbd5a2016-06-16 09:17:49 -07003894 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003895 changes = [Changelist(branchref=b, auth_config=auth_config)
3896 for b in branches.splitlines()]
3897 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3898 statuses = get_cl_statuses(changes,
3899 fine_grained=True,
3900 max_processes=options.maxjobs)
3901 proposal = [(cl.GetBranch(),
3902 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3903 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003904 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003905 proposal.sort()
3906
3907 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003908 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003909 return 0
3910
3911 current_branch = GetCurrentBranch()
3912
vapiera7fbd5a2016-06-16 09:17:49 -07003913 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003914 if options.notags:
3915 for next_item in proposal:
3916 print(' ' + next_item[0])
3917 else:
3918 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3919 for next_item in proposal:
3920 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003921
kmarshall9249e012016-08-23 12:02:16 -07003922 # Quit now on precondition failure or if instructed by the user, either
3923 # via an interactive prompt or by command line flags.
3924 if options.dry_run:
3925 print('\nNo changes were made (dry run).\n')
3926 return 0
3927 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003928 print('You are currently on a branch \'%s\' which is associated with a '
3929 'closed codereview issue, so archive cannot proceed. Please '
3930 'checkout another branch and run this command again.' %
3931 current_branch)
3932 return 1
kmarshall9249e012016-08-23 12:02:16 -07003933 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003934 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3935 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003936 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003937 return 1
3938
3939 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003940 if not options.notags:
3941 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003942 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003943
vapiera7fbd5a2016-06-16 09:17:49 -07003944 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003945
3946 return 0
3947
3948
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003949@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003950def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003951 """Show status of changelists.
3952
3953 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003954 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003955 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003956 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003957 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003958 - Magenta in the commit queue
3959 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003960 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003961
3962 Also see 'git cl comments'.
3963 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003964 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003965 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003966 parser.add_option('-f', '--fast', action='store_true',
3967 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003968 parser.add_option(
3969 '-j', '--maxjobs', action='store', type=int,
3970 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003971
3972 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003973 _add_codereview_issue_select_options(
3974 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003975 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003976 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003977 if args:
3978 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003979 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003980
iannuccie53c9352016-08-17 14:40:40 -07003981 if options.issue is not None and not options.field:
3982 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003983
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003985 cl = Changelist(auth_config=auth_config, issue=options.issue,
3986 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003987 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003988 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003989 elif options.field == 'id':
3990 issueid = cl.GetIssue()
3991 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003992 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003993 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003994 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003995 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003996 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003997 elif options.field == 'status':
3998 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003999 elif options.field == 'url':
4000 url = cl.GetIssueURL()
4001 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004002 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004003 return 0
4004
4005 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4006 if not branches:
4007 print('No local branch found.')
4008 return 0
4009
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004010 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004011 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004012 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004013 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004014 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004015 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004016 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004017
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004018 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004019 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4020 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4021 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004022 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004023 c, status = output.next()
4024 branch_statuses[c.GetBranch()] = status
4025 status = branch_statuses.pop(branch)
4026 url = cl.GetIssueURL()
4027 if url and (not status or status == 'error'):
4028 # The issue probably doesn't exist anymore.
4029 url += ' (broken)'
4030
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004031 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004032 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004033 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004034 color = ''
4035 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004036 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004037 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004038 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004039 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004040
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004041
4042 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004043 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004044 print('Current branch: %s' % branch)
4045 for cl in changes:
4046 if cl.GetBranch() == branch:
4047 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004048 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004049 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004050 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004051 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004052 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004053 print('Issue description:')
4054 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004055 return 0
4056
4057
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004058def colorize_CMDstatus_doc():
4059 """To be called once in main() to add colors to git cl status help."""
4060 colors = [i for i in dir(Fore) if i[0].isupper()]
4061
4062 def colorize_line(line):
4063 for color in colors:
4064 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004065 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004066 indent = len(line) - len(line.lstrip(' ')) + 1
4067 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4068 return line
4069
4070 lines = CMDstatus.__doc__.splitlines()
4071 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4072
4073
phajdan.jre328cf92016-08-22 04:12:17 -07004074def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004075 if path == '-':
4076 json.dump(contents, sys.stdout)
4077 else:
4078 with open(path, 'w') as f:
4079 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004080
4081
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004082@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004083@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004084def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004085 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004086
4087 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004088 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004089 parser.add_option('-r', '--reverse', action='store_true',
4090 help='Lookup the branch(es) for the specified issues. If '
4091 'no issues are specified, all branches with mapped '
4092 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004093 parser.add_option('--json',
4094 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004095 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004096 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004097 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004098
dnj@chromium.org406c4402015-03-03 17:22:28 +00004099 if options.reverse:
4100 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004101 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004102 # Reverse issue lookup.
4103 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004104
4105 git_config = {}
4106 for config in RunGit(['config', '--get-regexp',
4107 r'branch\..*issue']).splitlines():
4108 name, _space, val = config.partition(' ')
4109 git_config[name] = val
4110
dnj@chromium.org406c4402015-03-03 17:22:28 +00004111 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004112 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4113 config_key = _git_branch_config_key(ShortBranchName(branch),
4114 cls.IssueConfigKey())
4115 issue = git_config.get(config_key)
4116 if issue:
4117 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004118 if not args:
4119 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004120 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004121 for issue in args:
4122 if not issue:
4123 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004124 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004125 print('Branch for issue number %s: %s' % (
4126 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004127 if options.json:
4128 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004129 return 0
4130
4131 if len(args) > 0:
4132 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4133 if not issue.valid:
4134 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4135 'or no argument to list it.\n'
4136 'Maybe you want to run git cl status?')
4137 cl = Changelist(codereview=issue.codereview)
4138 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004139 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004140 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004141 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4142 if options.json:
4143 write_json(options.json, {
4144 'issue': cl.GetIssue(),
4145 'issue_url': cl.GetIssueURL(),
4146 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004147 return 0
4148
4149
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004150@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004151def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004152 """Shows or posts review comments for any changelist."""
4153 parser.add_option('-a', '--add-comment', dest='comment',
4154 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004155 parser.add_option('-p', '--publish', action='store_true',
4156 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004157 parser.add_option('-i', '--issue', dest='issue',
4158 help='review issue id (defaults to current issue). '
4159 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004160 parser.add_option('-m', '--machine-readable', dest='readable',
4161 action='store_false', default=True,
4162 help='output comments in a format compatible with '
4163 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004164 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004165 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004166 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004167 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004168 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004169 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004170 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004171
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004172 issue = None
4173 if options.issue:
4174 try:
4175 issue = int(options.issue)
4176 except ValueError:
4177 DieWithError('A review issue id is expected to be a number')
4178
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004179 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4180
4181 if not cl.IsGerrit():
4182 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004183
4184 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004185 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004186 return 0
4187
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004188 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4189 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004190 for comment in summary:
4191 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004192 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004193 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004194 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004195 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004196 color = Fore.MAGENTA
4197 else:
4198 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004199 print('\n%s%s %s%s\n%s' % (
4200 color,
4201 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4202 comment.sender,
4203 Fore.RESET,
4204 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4205
smut@google.comc85ac942015-09-15 16:34:43 +00004206 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004207 def pre_serialize(c):
4208 dct = c.__dict__.copy()
4209 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4210 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004211 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004212 return 0
4213
4214
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004215@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004216@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004217def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004218 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004219 parser.add_option('-d', '--display', action='store_true',
4220 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004221 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004222 help='New description to set for this issue (- for stdin, '
4223 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004224 parser.add_option('-f', '--force', action='store_true',
4225 help='Delete any unpublished Gerrit edits for this issue '
4226 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004227
4228 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004229 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004230 options, args = parser.parse_args(args)
4231 _process_codereview_select_options(parser, options)
4232
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004233 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004234 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004235 target_issue_arg = ParseIssueNumberArgument(args[0],
4236 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004237 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004238 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004239
martiniss6eda05f2016-06-30 10:18:35 -07004240 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004241 'auth_config': auth.extract_auth_config_from_options(options),
4242 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004243 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004244 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004245 if target_issue_arg:
4246 kwargs['issue'] = target_issue_arg.issue
4247 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004248 if target_issue_arg.codereview and not options.forced_codereview:
4249 detected_codereview_from_url = True
4250 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004251
4252 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004253 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004254 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004255 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004256
4257 if detected_codereview_from_url:
4258 logging.info('canonical issue/change URL: %s (type: %s)\n',
4259 cl.GetIssueURL(), target_issue_arg.codereview)
4260
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004261 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004262
smut@google.com34fb6b12015-07-13 20:03:26 +00004263 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004264 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004265 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004266
4267 if options.new_description:
4268 text = options.new_description
4269 if text == '-':
4270 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004271 elif text == '+':
4272 base_branch = cl.GetCommonAncestorWithUpstream()
4273 change = cl.GetChange(base_branch, None, local_description=True)
4274 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004275
4276 description.set_description(text)
4277 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004278 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004279
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004280 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004281 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004282 return 0
4283
4284
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004285@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004286def CMDlint(parser, args):
4287 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004288 parser.add_option('--filter', action='append', metavar='-x,+y',
4289 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004290 auth.add_auth_options(parser)
4291 options, args = parser.parse_args(args)
4292 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004293
4294 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004295 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004296 try:
4297 import cpplint
4298 import cpplint_chromium
4299 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004300 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004301 return 1
4302
4303 # Change the current working directory before calling lint so that it
4304 # shows the correct base.
4305 previous_cwd = os.getcwd()
4306 os.chdir(settings.GetRoot())
4307 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004308 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004309 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4310 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004311 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004312 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004313 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004314
4315 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004316 command = args + files
4317 if options.filter:
4318 command = ['--filter=' + ','.join(options.filter)] + command
4319 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004320
4321 white_regex = re.compile(settings.GetLintRegex())
4322 black_regex = re.compile(settings.GetLintIgnoreRegex())
4323 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4324 for filename in filenames:
4325 if white_regex.match(filename):
4326 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004327 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004328 else:
4329 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4330 extra_check_functions)
4331 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004332 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004333 finally:
4334 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004335 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004336 if cpplint._cpplint_state.error_count != 0:
4337 return 1
4338 return 0
4339
4340
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004341@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004342def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004343 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004344 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004345 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004346 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004347 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004348 parser.add_option('--all', action='store_true',
4349 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004350 parser.add_option('--parallel', action='store_true',
4351 help='Run all tests specified by input_api.RunTests in all '
4352 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004353 auth.add_auth_options(parser)
4354 options, args = parser.parse_args(args)
4355 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004356
sbc@chromium.org71437c02015-04-09 19:29:40 +00004357 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004358 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004359 return 1
4360
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004361 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004362 if args:
4363 base_branch = args[0]
4364 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004365 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004366 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004367
Aaron Gable8076c282017-11-29 14:39:41 -08004368 if options.all:
4369 base_change = cl.GetChange(base_branch, None)
4370 files = [('M', f) for f in base_change.AllFiles()]
4371 change = presubmit_support.GitChange(
4372 base_change.Name(),
4373 base_change.FullDescriptionText(),
4374 base_change.RepositoryRoot(),
4375 files,
4376 base_change.issue,
4377 base_change.patchset,
4378 base_change.author_email,
4379 base_change._upstream)
4380 else:
4381 change = cl.GetChange(base_branch, None)
4382
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004383 cl.RunHook(
4384 committing=not options.upload,
4385 may_prompt=False,
4386 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004387 change=change,
4388 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004389 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004390
4391
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004392def GenerateGerritChangeId(message):
4393 """Returns Ixxxxxx...xxx change id.
4394
4395 Works the same way as
4396 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4397 but can be called on demand on all platforms.
4398
4399 The basic idea is to generate git hash of a state of the tree, original commit
4400 message, author/committer info and timestamps.
4401 """
4402 lines = []
4403 tree_hash = RunGitSilent(['write-tree'])
4404 lines.append('tree %s' % tree_hash.strip())
4405 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4406 if code == 0:
4407 lines.append('parent %s' % parent.strip())
4408 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4409 lines.append('author %s' % author.strip())
4410 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4411 lines.append('committer %s' % committer.strip())
4412 lines.append('')
4413 # Note: Gerrit's commit-hook actually cleans message of some lines and
4414 # whitespace. This code is not doing this, but it clearly won't decrease
4415 # entropy.
4416 lines.append(message)
4417 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4418 stdin='\n'.join(lines))
4419 return 'I%s' % change_hash.strip()
4420
4421
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004422def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004423 """Computes the remote branch ref to use for the CL.
4424
4425 Args:
4426 remote (str): The git remote for the CL.
4427 remote_branch (str): The git remote branch for the CL.
4428 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004429 """
4430 if not (remote and remote_branch):
4431 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004432
wittman@chromium.org455dc922015-01-26 20:15:50 +00004433 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004434 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004435 # refs, which are then translated into the remote full symbolic refs
4436 # below.
4437 if '/' not in target_branch:
4438 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4439 else:
4440 prefix_replacements = (
4441 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4442 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4443 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4444 )
4445 match = None
4446 for regex, replacement in prefix_replacements:
4447 match = re.search(regex, target_branch)
4448 if match:
4449 remote_branch = target_branch.replace(match.group(0), replacement)
4450 break
4451 if not match:
4452 # This is a branch path but not one we recognize; use as-is.
4453 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004454 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4455 # Handle the refs that need to land in different refs.
4456 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004457
wittman@chromium.org455dc922015-01-26 20:15:50 +00004458 # Create the true path to the remote branch.
4459 # Does the following translation:
4460 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4461 # * refs/remotes/origin/master -> refs/heads/master
4462 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4463 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4464 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4465 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4466 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4467 'refs/heads/')
4468 elif remote_branch.startswith('refs/remotes/branch-heads'):
4469 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004470
wittman@chromium.org455dc922015-01-26 20:15:50 +00004471 return remote_branch
4472
4473
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004474def cleanup_list(l):
4475 """Fixes a list so that comma separated items are put as individual items.
4476
4477 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4478 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4479 """
4480 items = sum((i.split(',') for i in l), [])
4481 stripped_items = (i.strip() for i in items)
4482 return sorted(filter(None, stripped_items))
4483
4484
Aaron Gable4db38df2017-11-03 14:59:07 -07004485@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004486@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004487def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004488 """Uploads the current changelist to codereview.
4489
4490 Can skip dependency patchset uploads for a branch by running:
4491 git config branch.branch_name.skip-deps-uploads True
4492 To unset run:
4493 git config --unset branch.branch_name.skip-deps-uploads
4494 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004495
4496 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4497 a bug number, this bug number is automatically populated in the CL
4498 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004499
4500 If subject contains text in square brackets or has "<text>: " prefix, such
4501 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4502 [git-cl] add support for hashtags
4503 Foo bar: implement foo
4504 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004505 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004506 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4507 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004508 parser.add_option('--bypass-watchlists', action='store_true',
4509 dest='bypass_watchlists',
4510 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004511 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004512 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004513 parser.add_option('--message', '-m', dest='message',
4514 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004515 parser.add_option('-b', '--bug',
4516 help='pre-populate the bug number(s) for this issue. '
4517 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004518 parser.add_option('--message-file', dest='message_file',
4519 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004520 parser.add_option('--title', '-t', dest='title',
4521 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004522 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004523 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004524 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004525 parser.add_option('--tbrs',
4526 action='append', default=[],
4527 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004528 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004529 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004530 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004531 parser.add_option('--hashtag', dest='hashtags',
4532 action='append', default=[],
4533 help=('Gerrit hashtag for new CL; '
4534 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004535 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004536 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004537 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004538 help='tell the commit queue to commit this patchset; '
4539 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004540 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004541 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004542 metavar='TARGET',
4543 help='Apply CL to remote ref TARGET. ' +
4544 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004545 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004546 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004547 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004548 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004549 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004550 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004551 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4552 const='TBR', help='add a set of OWNERS to TBR')
4553 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4554 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004555 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4556 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004557 help='Send the patchset to do a CQ dry run right after '
4558 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004559 parser.add_option('--dependencies', action='store_true',
4560 help='Uploads CLs of all the local branches that depend on '
4561 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004562 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4563 help='Sends your change to the CQ after an approval. Only '
4564 'works on repos that have the Auto-Submit label '
4565 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004566 parser.add_option('--parallel', action='store_true',
4567 help='Run all tests specified by input_api.RunTests in all '
4568 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004569
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004570 parser.add_option('--no-autocc', action='store_true',
4571 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004572 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004573 help='Set the review private. This implies --no-autocc.')
4574
rmistry@google.com2dd99862015-06-22 12:22:18 +00004575 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004576 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004577 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004578 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004579 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004580 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004581
sbc@chromium.org71437c02015-04-09 19:29:40 +00004582 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004583 return 1
4584
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004585 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004586 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004587 options.cc = cleanup_list(options.cc)
4588
tandriib80458a2016-06-23 12:20:07 -07004589 if options.message_file:
4590 if options.message:
4591 parser.error('only one of --message and --message-file allowed.')
4592 options.message = gclient_utils.FileRead(options.message_file)
4593 options.message_file = None
4594
tandrii4d0545a2016-07-06 03:56:49 -07004595 if options.cq_dry_run and options.use_commit_queue:
4596 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4597
Aaron Gableedbc4132017-09-11 13:22:28 -07004598 if options.use_commit_queue:
4599 options.send_mail = True
4600
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004601 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4602 settings.GetIsGerrit()
4603
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004604 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004605 if not cl.IsGerrit():
4606 # Error out with instructions for repos not yet configured for Gerrit.
4607 print('=====================================')
4608 print('NOTICE: Rietveld is no longer supported. '
4609 'You can upload changes to Gerrit with')
4610 print(' git cl upload --gerrit')
4611 print('or set Gerrit to be your default code review tool with')
4612 print(' git config gerrit.host true')
4613 print('=====================================')
4614 return 1
4615
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004616 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004617
4618
Francois Dorayd42c6812017-05-30 15:10:20 -04004619@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004620@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004621def CMDsplit(parser, args):
4622 """Splits a branch into smaller branches and uploads CLs.
4623
4624 Creates a branch and uploads a CL for each group of files modified in the
4625 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004626 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004627 the shared OWNERS file.
4628 """
4629 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004630 help="A text file containing a CL description in which "
4631 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004632 parser.add_option("-c", "--comment", dest="comment_file",
4633 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004634 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4635 default=False,
4636 help="List the files and reviewers for each CL that would "
4637 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004638 parser.add_option("--cq-dry-run", action='store_true',
4639 help="If set, will do a cq dry run for each uploaded CL. "
4640 "Please be careful when doing this; more than ~10 CLs "
4641 "has the potential to overload our build "
4642 "infrastructure. Try to upload these not during high "
4643 "load times (usually 11-3 Mountain View time). Email "
4644 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004645 options, _ = parser.parse_args(args)
4646
4647 if not options.description_file:
4648 parser.error('No --description flag specified.')
4649
4650 def WrappedCMDupload(args):
4651 return CMDupload(OptionParser(), args)
4652
4653 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004654 Changelist, WrappedCMDupload, options.dry_run,
4655 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04004656
4657
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004658@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004659@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004660def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004661 """DEPRECATED: Used to commit the current changelist via git-svn."""
4662 message = ('git-cl no longer supports committing to SVN repositories via '
4663 'git-svn. You probably want to use `git cl land` instead.')
4664 print(message)
4665 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004666
4667
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004668# Two special branches used by git cl land.
4669MERGE_BRANCH = 'git-cl-commit'
4670CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4671
4672
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004673@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004674@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004675def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004676 """Commits the current changelist via git.
4677
4678 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4679 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004680 """
4681 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4682 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004683 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004684 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004685 parser.add_option('--parallel', action='store_true',
4686 help='Run all tests specified by input_api.RunTests in all '
4687 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004688 auth.add_auth_options(parser)
4689 (options, args) = parser.parse_args(args)
4690 auth_config = auth.extract_auth_config_from_options(options)
4691
4692 cl = Changelist(auth_config=auth_config)
4693
Robert Iannucci2e73d432018-03-14 01:10:47 -07004694 if not cl.IsGerrit():
4695 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004696
Robert Iannucci2e73d432018-03-14 01:10:47 -07004697 if not cl.GetIssue():
4698 DieWithError('You must upload the change first to Gerrit.\n'
4699 ' If you would rather have `git cl land` upload '
4700 'automatically for you, see http://crbug.com/642759')
4701 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004702 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004703
4704
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004705@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004706@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004707def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004708 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004709 parser.add_option('-b', dest='newbranch',
4710 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004711 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004712 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004713 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004714 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004715 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004716 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004717 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004718 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004719 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004720 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004721
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004722
4723 group = optparse.OptionGroup(
4724 parser,
4725 'Options for continuing work on the current issue uploaded from a '
4726 'different clone (e.g. different machine). Must be used independently '
4727 'from the other options. No issue number should be specified, and the '
4728 'branch must have an issue number associated with it')
4729 group.add_option('--reapply', action='store_true', dest='reapply',
4730 help='Reset the branch and reapply the issue.\n'
4731 'CAUTION: This will undo any local changes in this '
4732 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004733
4734 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004735 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004736 parser.add_option_group(group)
4737
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004738 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004739 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004740 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004741 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004742 auth_config = auth.extract_auth_config_from_options(options)
4743
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004744 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004745 if options.newbranch:
4746 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004747 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004748 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004749
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004750 cl = Changelist(auth_config=auth_config,
4751 codereview=options.forced_codereview)
4752 if not cl.GetIssue():
4753 parser.error('current branch must have an associated issue')
4754
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004755 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004756 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004757 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004758
4759 RunGit(['reset', '--hard', upstream])
4760 if options.pull:
4761 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004762
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004763 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4764 options.directory)
4765
4766 if len(args) != 1 or not args[0]:
4767 parser.error('Must specify issue number or url')
4768
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004769 target_issue_arg = ParseIssueNumberArgument(args[0],
4770 options.forced_codereview)
4771 if not target_issue_arg.valid:
4772 parser.error('invalid codereview url or CL id')
4773
4774 cl_kwargs = {
4775 'auth_config': auth_config,
4776 'codereview_host': target_issue_arg.hostname,
4777 'codereview': options.forced_codereview,
4778 }
4779 detected_codereview_from_url = False
4780 if target_issue_arg.codereview and not options.forced_codereview:
4781 detected_codereview_from_url = True
4782 cl_kwargs['codereview'] = target_issue_arg.codereview
4783 cl_kwargs['issue'] = target_issue_arg.issue
4784
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004785 # We don't want uncommitted changes mixed up with the patch.
4786 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004787 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004788
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004789 if options.newbranch:
4790 if options.force:
4791 RunGit(['branch', '-D', options.newbranch],
4792 stderr=subprocess2.PIPE, error_ok=True)
4793 RunGit(['new-branch', options.newbranch])
4794
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004795 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004796
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004797 if cl.IsGerrit():
4798 if options.reject:
4799 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004800 if options.directory:
4801 parser.error('--directory is not supported with Gerrit codereview.')
4802
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004803 if detected_codereview_from_url:
4804 print('canonical issue/change URL: %s (type: %s)\n' %
4805 (cl.GetIssueURL(), target_issue_arg.codereview))
4806
4807 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07004808 options.nocommit, options.directory,
4809 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004810
4811
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004812def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004813 """Fetches the tree status and returns either 'open', 'closed',
4814 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004815 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004816 if url:
4817 status = urllib2.urlopen(url).read().lower()
4818 if status.find('closed') != -1 or status == '0':
4819 return 'closed'
4820 elif status.find('open') != -1 or status == '1':
4821 return 'open'
4822 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004823 return 'unset'
4824
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004825
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004826def GetTreeStatusReason():
4827 """Fetches the tree status from a json url and returns the message
4828 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004829 url = settings.GetTreeStatusUrl()
4830 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004831 connection = urllib2.urlopen(json_url)
4832 status = json.loads(connection.read())
4833 connection.close()
4834 return status['message']
4835
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004836
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004837@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004838def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004839 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004840 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004841 status = GetTreeStatus()
4842 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004843 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004844 return 2
4845
vapiera7fbd5a2016-06-16 09:17:49 -07004846 print('The tree is %s' % status)
4847 print()
4848 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004849 if status != 'open':
4850 return 1
4851 return 0
4852
4853
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004854@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004855def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004856 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004857 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004858 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004859 '-b', '--bot', action='append',
4860 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4861 'times to specify multiple builders. ex: '
4862 '"-b win_rel -b win_layout". See '
4863 'the try server waterfall for the builders name and the tests '
4864 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004865 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004866 '-B', '--bucket', default='',
4867 help=('Buildbucket bucket to send the try requests.'))
4868 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004869 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004870 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004871 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004872 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004873 help='Revision to use for the try job; default: the revision will '
4874 'be determined by the try recipe that builder runs, which usually '
4875 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004876 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004877 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004878 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004879 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004880 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004881 '--category', default='git_cl_try', help='Specify custom build category.')
4882 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004883 '--project',
4884 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004885 'in recipe to determine to which repository or directory to '
4886 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004887 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004888 '-p', '--property', dest='properties', action='append', default=[],
4889 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004890 'key2=value2 etc. The value will be treated as '
4891 'json if decodable, or as string otherwise. '
4892 'NOTE: using this may make your try job not usable for CQ, '
4893 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004894 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004895 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4896 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004897 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004898 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09004899 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004900 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09004901 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004902 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004903
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004904 if options.master and options.master.startswith('luci.'):
4905 parser.error(
4906 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00004907 # Make sure that all properties are prop=value pairs.
4908 bad_params = [x for x in options.properties if '=' not in x]
4909 if bad_params:
4910 parser.error('Got properties with missing "=": %s' % bad_params)
4911
maruel@chromium.org15192402012-09-06 12:38:29 +00004912 if args:
4913 parser.error('Unknown arguments: %s' % args)
4914
Koji Ishii31c14782018-01-08 17:17:33 +09004915 cl = Changelist(auth_config=auth_config, issue=options.issue,
4916 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00004917 if not cl.GetIssue():
4918 parser.error('Need to upload first')
4919
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004920 if cl.IsGerrit():
4921 # HACK: warm up Gerrit change detail cache to save on RPCs.
4922 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4923
tandriie113dfd2016-10-11 10:20:12 -07004924 error_message = cl.CannotTriggerTryJobReason()
4925 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004926 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004927
borenet6c0efe62016-10-19 08:13:29 -07004928 if options.bucket and options.master:
4929 parser.error('Only one of --bucket and --master may be used.')
4930
qyearsley1fdfcb62016-10-24 13:22:03 -07004931 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004932
qyearsleydd49f942016-10-28 11:57:22 -07004933 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4934 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004935 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004936 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004937 print('git cl try with no bots now defaults to CQ dry run.')
4938 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4939 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004940
borenet6c0efe62016-10-19 08:13:29 -07004941 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004942 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004943 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004944 'of bot requires an initial job from a parent (usually a builder). '
4945 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004946 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004947 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004948
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004949 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07004950 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004951 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07004952 except BuildbucketResponseException as ex:
4953 print('ERROR: %s' % ex)
4954 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004955 return 0
4956
4957
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004958@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004959def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004960 """Prints info about try jobs associated with current CL."""
4961 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004962 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004963 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004964 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004965 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004966 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004967 '--color', action='store_true', default=setup_color.IS_TTY,
4968 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004969 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004970 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4971 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004972 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07004973 '--json', help=('Path of JSON output file to write try job results to,'
4974 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004975 parser.add_option_group(group)
4976 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07004977 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004978 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07004979 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004980 if args:
4981 parser.error('Unrecognized args: %s' % ' '.join(args))
4982
4983 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07004984 cl = Changelist(
4985 issue=options.issue, codereview=options.forced_codereview,
4986 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004987 if not cl.GetIssue():
4988 parser.error('Need to upload first')
4989
tandrii221ab252016-10-06 08:12:04 -07004990 patchset = options.patchset
4991 if not patchset:
4992 patchset = cl.GetMostRecentPatchset()
4993 if not patchset:
4994 parser.error('Codereview doesn\'t know about issue %s. '
4995 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004996 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07004997 cl.GetIssue())
4998
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004999 try:
tandrii221ab252016-10-06 08:12:04 -07005000 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005001 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005002 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005003 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005004 if options.json:
5005 write_try_results_json(options.json, jobs)
5006 else:
5007 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005008 return 0
5009
5010
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005011@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005012@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005013def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005014 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005015 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005016 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005017 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005018
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005019 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005020 if args:
5021 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005022 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005023 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005024 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005025 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005026
5027 # Clear configured merge-base, if there is one.
5028 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005029 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005030 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005031 return 0
5032
5033
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005034@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005035def CMDweb(parser, args):
5036 """Opens the current CL in the web browser."""
5037 _, args = parser.parse_args(args)
5038 if args:
5039 parser.error('Unrecognized args: %s' % ' '.join(args))
5040
5041 issue_url = Changelist().GetIssueURL()
5042 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005043 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005044 return 1
5045
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005046 # Redirect I/O before invoking browser to hide its output. For example, this
5047 # allows to hide "Created new window in existing browser session." message
5048 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5049 saved_stdout = os.dup(1)
5050 os.close(1)
5051 os.open(os.devnull, os.O_RDWR)
5052 try:
5053 webbrowser.open(issue_url)
5054 finally:
5055 os.dup2(saved_stdout, 1)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005056 return 0
5057
5058
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005059@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005060def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005061 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005062 parser.add_option('-d', '--dry-run', action='store_true',
5063 help='trigger in dry run mode')
5064 parser.add_option('-c', '--clear', action='store_true',
5065 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005066 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005067 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005068 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005069 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005070 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005071 if args:
5072 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005073 if options.dry_run and options.clear:
5074 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5075
iannuccie53c9352016-08-17 14:40:40 -07005076 cl = Changelist(auth_config=auth_config, issue=options.issue,
5077 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005078 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005079 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005080 elif options.dry_run:
5081 state = _CQState.DRY_RUN
5082 else:
5083 state = _CQState.COMMIT
5084 if not cl.GetIssue():
5085 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005086 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005087 return 0
5088
5089
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005090@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005091def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005092 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005093 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005094 auth.add_auth_options(parser)
5095 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005096 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005097 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005098 if args:
5099 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005100 cl = Changelist(auth_config=auth_config, issue=options.issue,
5101 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005102 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005103 if not cl.GetIssue():
5104 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005105 cl.CloseIssue()
5106 return 0
5107
5108
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005109@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005110def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005111 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005112 parser.add_option(
5113 '--stat',
5114 action='store_true',
5115 dest='stat',
5116 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005117 auth.add_auth_options(parser)
5118 options, args = parser.parse_args(args)
5119 auth_config = auth.extract_auth_config_from_options(options)
5120 if args:
5121 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005122
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005123 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005124 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005125 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005126 if not issue:
5127 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005128
Aaron Gablea718c3e2017-08-28 17:47:28 -07005129 base = cl._GitGetBranchConfigValue('last-upload-hash')
5130 if not base:
5131 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5132 if not base:
5133 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5134 revision_info = detail['revisions'][detail['current_revision']]
5135 fetch_info = revision_info['fetch']['http']
5136 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5137 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005138
Aaron Gablea718c3e2017-08-28 17:47:28 -07005139 cmd = ['git', 'diff']
5140 if options.stat:
5141 cmd.append('--stat')
5142 cmd.append(base)
5143 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005144
5145 return 0
5146
5147
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005148@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005149def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005150 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005151 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005152 '--ignore-current',
5153 action='store_true',
5154 help='Ignore the CL\'s current reviewers and start from scratch.')
5155 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005156 '--no-color',
5157 action='store_true',
5158 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005159 parser.add_option(
5160 '--batch',
5161 action='store_true',
5162 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005163 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005164 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005165 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005166
5167 author = RunGit(['config', 'user.email']).strip() or None
5168
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005169 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005170
5171 if args:
5172 if len(args) > 1:
5173 parser.error('Unknown args')
5174 base_branch = args[0]
5175 else:
5176 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005177 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005178
5179 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005180 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5181
5182 if options.batch:
5183 db = owners.Database(change.RepositoryRoot(), file, os.path)
5184 print('\n'.join(db.reviewers_for(affected_files, author)))
5185 return 0
5186
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005187 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005188 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005189 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005190 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005191 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005192 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005193 disable_color=options.no_color,
5194 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005195
5196
Aiden Bennerc08566e2018-10-03 17:52:42 +00005197def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005198 """Generates a diff command."""
5199 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005200 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5201
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005202 if allow_prefix:
5203 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5204 # case that diff.noprefix is set in the user's git config.
5205 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5206 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005207 diff_cmd += ['--no-prefix']
5208
5209 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005210
5211 if args:
5212 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005213 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005214 diff_cmd.append(arg)
5215 else:
5216 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005217
5218 return diff_cmd
5219
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005220
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005221def MatchingFileType(file_name, extensions):
5222 """Returns true if the file name ends with one of the given extensions."""
5223 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005224
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005225
enne@chromium.org555cfe42014-01-29 18:21:39 +00005226@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005227@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005228def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005229 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005230 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005231 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005232 parser.add_option('--full', action='store_true',
5233 help='Reformat the full content of all touched files')
5234 parser.add_option('--dry-run', action='store_true',
5235 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005236 parser.add_option(
5237 '--python',
5238 action='store_true',
5239 default=None,
5240 help='Enables python formatting on all python files.')
5241 parser.add_option(
5242 '--no-python',
5243 action='store_true',
5244 dest='python',
5245 help='Disables python formatting on all python files. '
5246 'Takes precedence over --python. '
5247 'If neither --python or --no-python are set, python '
5248 'files that have a .style.yapf file in an ancestor '
5249 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005250 parser.add_option('--js', action='store_true',
5251 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005252 parser.add_option('--diff', action='store_true',
5253 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005254 parser.add_option('--presubmit', action='store_true',
5255 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005256 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005257
Daniel Chengc55eecf2016-12-30 03:11:02 -08005258 # Normalize any remaining args against the current path, so paths relative to
5259 # the current directory are still resolved as expected.
5260 args = [os.path.join(os.getcwd(), arg) for arg in args]
5261
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005262 # git diff generates paths against the root of the repository. Change
5263 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005264 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005265 if rel_base_path:
5266 os.chdir(rel_base_path)
5267
digit@chromium.org29e47272013-05-17 17:01:46 +00005268 # Grab the merge-base commit, i.e. the upstream commit of the current
5269 # branch when it was created or the last time it was rebased. This is
5270 # to cover the case where the user may have called "git fetch origin",
5271 # moving the origin branch to a newer commit, but hasn't rebased yet.
5272 upstream_commit = None
5273 cl = Changelist()
5274 upstream_branch = cl.GetUpstreamBranch()
5275 if upstream_branch:
5276 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5277 upstream_commit = upstream_commit.strip()
5278
5279 if not upstream_commit:
5280 DieWithError('Could not find base commit for this branch. '
5281 'Are you in detached state?')
5282
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005283 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5284 diff_output = RunGit(changed_files_cmd)
5285 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005286 # Filter out files deleted by this CL
5287 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005288
Christopher Lamc5ba6922017-01-24 11:19:14 +11005289 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005290 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005291
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005292 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5293 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5294 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005295 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005296
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005297 top_dir = os.path.normpath(
5298 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5299
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005300 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5301 # formatted. This is used to block during the presubmit.
5302 return_value = 0
5303
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005304 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005305 # Locate the clang-format binary in the checkout
5306 try:
5307 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005308 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005309 DieWithError(e)
5310
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005311 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005312 cmd = [clang_format_tool]
5313 if not opts.dry_run and not opts.diff:
5314 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005315 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005316 if opts.diff:
5317 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005318 else:
5319 env = os.environ.copy()
5320 env['PATH'] = str(os.path.dirname(clang_format_tool))
5321 try:
5322 script = clang_format.FindClangFormatScriptInChromiumTree(
5323 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005324 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005325 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005326
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005327 cmd = [sys.executable, script, '-p0']
5328 if not opts.dry_run and not opts.diff:
5329 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005330
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005331 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5332 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005333
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005334 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5335 if opts.diff:
5336 sys.stdout.write(stdout)
5337 if opts.dry_run and len(stdout) > 0:
5338 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005339
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005340 # Similar code to above, but using yapf on .py files rather than clang-format
5341 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005342 py_explicitly_disabled = opts.python is not None and not opts.python
5343 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005344 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5345 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5346 if sys.platform.startswith('win'):
5347 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005348
Aiden Bennerc08566e2018-10-03 17:52:42 +00005349 # If we couldn't find a yapf file we'll default to the chromium style
5350 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005351 chromium_default_yapf_style = os.path.join(depot_tools_path,
5352 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005353 # Used for caching.
5354 yapf_configs = {}
5355 for f in python_diff_files:
5356 # Find the yapf style config for the current file, defaults to depot
5357 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005358 _FindYapfConfigFile(f, yapf_configs, top_dir)
5359
5360 # Turn on python formatting by default if a yapf config is specified.
5361 # This breaks in the case of this repo though since the specified
5362 # style file is also the global default.
5363 if opts.python is None:
5364 filtered_py_files = []
5365 for f in python_diff_files:
5366 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5367 filtered_py_files.append(f)
5368 else:
5369 filtered_py_files = python_diff_files
5370
5371 # Note: yapf still seems to fix indentation of the entire file
5372 # even if line ranges are specified.
5373 # See https://github.com/google/yapf/issues/499
5374 if not opts.full and filtered_py_files:
5375 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5376
5377 for f in filtered_py_files:
5378 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5379 if yapf_config is None:
5380 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005381
5382 cmd = [yapf_tool, '--style', yapf_config, f]
5383
5384 has_formattable_lines = False
5385 if not opts.full:
5386 # Only run yapf over changed line ranges.
5387 for diff_start, diff_len in py_line_diffs[f]:
5388 diff_end = diff_start + diff_len - 1
5389 # Yapf errors out if diff_end < diff_start but this
5390 # is a valid line range diff for a removal.
5391 if diff_end >= diff_start:
5392 has_formattable_lines = True
5393 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5394 # If all line diffs were removals we have nothing to format.
5395 if not has_formattable_lines:
5396 continue
5397
5398 if opts.diff or opts.dry_run:
5399 cmd += ['--diff']
5400 # Will return non-zero exit code if non-empty diff.
5401 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5402 if opts.diff:
5403 sys.stdout.write(stdout)
5404 elif len(stdout) > 0:
5405 return_value = 2
5406 else:
5407 cmd += ['-i']
5408 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005409
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005410 # Dart's formatter does not have the nice property of only operating on
5411 # modified chunks, so hard code full.
5412 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005413 try:
5414 command = [dart_format.FindDartFmtToolInChromiumTree()]
5415 if not opts.dry_run and not opts.diff:
5416 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005417 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005418
ppi@chromium.org6593d932016-03-03 15:41:15 +00005419 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005420 if opts.dry_run and stdout:
5421 return_value = 2
5422 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005423 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5424 'found in this checkout. Files in other languages are still '
5425 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005426
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005427 # Format GN build files. Always run on full build files for canonical form.
5428 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005429 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005430 if opts.dry_run or opts.diff:
5431 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005432 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005433 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5434 shell=sys.platform == 'win32',
5435 cwd=top_dir)
5436 if opts.dry_run and gn_ret == 2:
5437 return_value = 2 # Not formatted.
5438 elif opts.diff and gn_ret == 2:
5439 # TODO this should compute and print the actual diff.
5440 print("This change has GN build file diff for " + gn_diff_file)
5441 elif gn_ret != 0:
5442 # For non-dry run cases (and non-2 return values for dry-run), a
5443 # nonzero error code indicates a failure, probably because the file
5444 # doesn't parse.
5445 DieWithError("gn format failed on " + gn_diff_file +
5446 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005447
Ilya Shermane081cbe2017-08-15 17:51:04 -07005448 # Skip the metrics formatting from the global presubmit hook. These files have
5449 # a separate presubmit hook that issues an error if the files need formatting,
5450 # whereas the top-level presubmit script merely issues a warning. Formatting
5451 # these files is somewhat slow, so it's important not to duplicate the work.
5452 if not opts.presubmit:
5453 for xml_dir in GetDirtyMetricsDirs(diff_files):
5454 tool_dir = os.path.join(top_dir, xml_dir)
5455 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5456 if opts.dry_run or opts.diff:
5457 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005458 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005459 if opts.diff:
5460 sys.stdout.write(stdout)
5461 if opts.dry_run and stdout:
5462 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005463
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005464 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005465
Steven Holte2e664bf2017-04-21 13:10:47 -07005466def GetDirtyMetricsDirs(diff_files):
5467 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5468 metrics_xml_dirs = [
5469 os.path.join('tools', 'metrics', 'actions'),
5470 os.path.join('tools', 'metrics', 'histograms'),
5471 os.path.join('tools', 'metrics', 'rappor'),
5472 os.path.join('tools', 'metrics', 'ukm')]
5473 for xml_dir in metrics_xml_dirs:
5474 if any(file.startswith(xml_dir) for file in xml_diff_files):
5475 yield xml_dir
5476
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005477
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005478@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005479@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005480def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005481 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005482 _, args = parser.parse_args(args)
5483
5484 if len(args) != 1:
5485 parser.print_help()
5486 return 1
5487
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005488 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005489 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005490 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005491
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005492 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005493
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005494 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005495 output = RunGit(['config', '--local', '--get-regexp',
5496 r'branch\..*\.%s' % issueprefix],
5497 error_ok=True)
5498 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005499 if issue == target_issue:
5500 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005501
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005502 branches = []
5503 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005504 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005505 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005506 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005507 return 1
5508 if len(branches) == 1:
5509 RunGit(['checkout', branches[0]])
5510 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005511 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005512 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005513 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005514 which = raw_input('Choose by index: ')
5515 try:
5516 RunGit(['checkout', branches[int(which)]])
5517 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005518 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005519 return 1
5520
5521 return 0
5522
5523
maruel@chromium.org29404b52014-09-08 22:58:00 +00005524def CMDlol(parser, args):
5525 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005526 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005527 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5528 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5529 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005530 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005531 return 0
5532
5533
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005534class OptionParser(optparse.OptionParser):
5535 """Creates the option parse and add --verbose support."""
5536 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005537 optparse.OptionParser.__init__(
5538 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005539 self.add_option(
5540 '-v', '--verbose', action='count', default=0,
5541 help='Use 2 times for more debugging info')
5542
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005543 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005544 try:
5545 return self._parse_args(args)
5546 finally:
5547 # Regardless of success or failure of args parsing, we want to report
5548 # metrics, but only after logging has been initialized (if parsing
5549 # succeeded).
5550 global settings
5551 settings = Settings()
5552
5553 if not metrics.DISABLE_METRICS_COLLECTION:
5554 # GetViewVCUrl ultimately calls logging method.
5555 project_url = settings.GetViewVCUrl().strip('/+')
5556 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5557 metrics.collector.add('project_urls', [project_url])
5558
5559 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005560 # Create an optparse.Values object that will store only the actual passed
5561 # options, without the defaults.
5562 actual_options = optparse.Values()
5563 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5564 # Create an optparse.Values object with the default options.
5565 options = optparse.Values(self.get_default_values().__dict__)
5566 # Update it with the options passed by the user.
5567 options._update_careful(actual_options.__dict__)
5568 # Store the options passed by the user in an _actual_options attribute.
5569 # We store only the keys, and not the values, since the values can contain
5570 # arbitrary information, which might be PII.
5571 metrics.collector.add('arguments', actual_options.__dict__.keys())
5572
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005573 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005574 logging.basicConfig(
5575 level=levels[min(options.verbose, len(levels) - 1)],
5576 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5577 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005578
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005579 return options, args
5580
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005581
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005582def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005583 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005584 print('\nYour python version %s is unsupported, please upgrade.\n' %
5585 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005586 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005587
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005588 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005589 dispatcher = subcommand.CommandDispatcher(__name__)
5590 try:
5591 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005592 except auth.AuthenticationError as e:
5593 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005594 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005595 if e.code != 500:
5596 raise
5597 DieWithError(
5598 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5599 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005600 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005601
5602
5603if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005604 # These affect sys.stdout so do it outside of main() to simplify mocks in
5605 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005606 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005607 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005608 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005609 sys.exit(main(sys.argv[1:]))