blob: c076d870c00130439e6fa1bd89e5478a69be66e3 [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:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001113 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1114 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1115 self._codereview = codereview
1116 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001117 return
1118
1119 # Automatic selection based on issue number set for a current branch.
1120 # Rietveld takes precedence over Gerrit.
1121 assert not self.issue
1122 # Whether we find issue or not, we are doing the lookup.
1123 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001124 if self.GetBranch():
1125 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1126 issue = _git_get_branch_config_value(
1127 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1128 if issue:
1129 self._codereview = codereview
1130 self._codereview_impl = cls(self, **kwargs)
1131 self.issue = int(issue)
1132 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001133
1134 # No issue is set for this branch, so decide based on repo-wide settings.
1135 return self._load_codereview_impl(
1136 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1137 **kwargs)
1138
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001139 def IsGerrit(self):
1140 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001141
1142 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001143 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001144
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001145 The return value is a string suitable for passing to git cl with the --cc
1146 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001147 """
1148 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001149 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001150 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001151 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1152 return self.cc
1153
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001154 def GetCCListWithoutDefault(self):
1155 """Return the users cc'd on this CL excluding default ones."""
1156 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001157 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001158 return self.cc
1159
Daniel Cheng7227d212017-11-17 08:12:37 -08001160 def ExtendCC(self, more_cc):
1161 """Extends the list of users to cc on this CL based on the changed files."""
1162 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163
1164 def GetBranch(self):
1165 """Returns the short branch name, e.g. 'master'."""
1166 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001167 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001168 if not branchref:
1169 return None
1170 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171 self.branch = ShortBranchName(self.branchref)
1172 return self.branch
1173
1174 def GetBranchRef(self):
1175 """Returns the full branch name, e.g. 'refs/heads/master'."""
1176 self.GetBranch() # Poke the lazy loader.
1177 return self.branchref
1178
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001179 def ClearBranch(self):
1180 """Clears cached branch data of this object."""
1181 self.branch = self.branchref = None
1182
tandrii5d48c322016-08-18 16:19:37 -07001183 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1184 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1185 kwargs['branch'] = self.GetBranch()
1186 return _git_get_branch_config_value(key, default, **kwargs)
1187
1188 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1189 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1190 assert self.GetBranch(), (
1191 'this CL must have an associated branch to %sset %s%s' %
1192 ('un' if value is None else '',
1193 key,
1194 '' if value is None else ' to %r' % value))
1195 kwargs['branch'] = self.GetBranch()
1196 return _git_set_branch_config_value(key, value, **kwargs)
1197
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001198 @staticmethod
1199 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001200 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 e.g. 'origin', 'refs/heads/master'
1202 """
1203 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001204 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1205
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001207 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001209 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1210 error_ok=True).strip()
1211 if upstream_branch:
1212 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001214 # Else, try to guess the origin remote.
1215 remote_branches = RunGit(['branch', '-r']).split()
1216 if 'origin/master' in remote_branches:
1217 # Fall back on origin/master if it exits.
1218 remote = 'origin'
1219 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001221 DieWithError(
1222 'Unable to determine default branch to diff against.\n'
1223 'Either pass complete "git diff"-style arguments, like\n'
1224 ' git cl upload origin/master\n'
1225 'or verify this branch is set up to track another \n'
1226 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227
1228 return remote, upstream_branch
1229
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001230 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001231 upstream_branch = self.GetUpstreamBranch()
1232 if not BranchExists(upstream_branch):
1233 DieWithError('The upstream for the current branch (%s) does not exist '
1234 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001235 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001236 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001237
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 def GetUpstreamBranch(self):
1239 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001240 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001242 upstream_branch = upstream_branch.replace('refs/heads/',
1243 'refs/remotes/%s/' % remote)
1244 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1245 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 self.upstream_branch = upstream_branch
1247 return self.upstream_branch
1248
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001249 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001250 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001251 remote, branch = None, self.GetBranch()
1252 seen_branches = set()
1253 while branch not in seen_branches:
1254 seen_branches.add(branch)
1255 remote, branch = self.FetchUpstreamTuple(branch)
1256 branch = ShortBranchName(branch)
1257 if remote != '.' or branch.startswith('refs/remotes'):
1258 break
1259 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001260 remotes = RunGit(['remote'], error_ok=True).split()
1261 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001262 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001263 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001264 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001265 logging.warn('Could not determine which remote this change is '
1266 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001267 else:
1268 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001269 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 branch = 'HEAD'
1271 if branch.startswith('refs/remotes'):
1272 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001273 elif branch.startswith('refs/branch-heads/'):
1274 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001275 else:
1276 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001277 return self._remote
1278
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001279 def GitSanityChecks(self, upstream_git_obj):
1280 """Checks git repo status and ensures diff is from local commits."""
1281
sbc@chromium.org79706062015-01-14 21:18:12 +00001282 if upstream_git_obj is None:
1283 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001284 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001285 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001286 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001287 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001288 return False
1289
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 # Verify the commit we're diffing against is in our current branch.
1291 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1292 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1293 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001294 print('ERROR: %s is not in the current branch. You may need to rebase '
1295 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001296 return False
1297
1298 # List the commits inside the diff, and verify they are all local.
1299 commits_in_diff = RunGit(
1300 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1301 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1302 remote_branch = remote_branch.strip()
1303 if code != 0:
1304 _, remote_branch = self.GetRemoteBranch()
1305
1306 commits_in_remote = RunGit(
1307 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1308
1309 common_commits = set(commits_in_diff) & set(commits_in_remote)
1310 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001311 print('ERROR: Your diff contains %d commits already in %s.\n'
1312 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1313 'the diff. If you are using a custom git flow, you can override'
1314 ' the reference used for this check with "git config '
1315 'gitcl.remotebranch <git-ref>".' % (
1316 len(common_commits), remote_branch, upstream_git_obj),
1317 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001318 return False
1319 return True
1320
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001321 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001322 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001323
1324 Returns None if it is not set.
1325 """
tandrii5d48c322016-08-18 16:19:37 -07001326 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001327
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 def GetRemoteUrl(self):
1329 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1330
1331 Returns None if there is no remote.
1332 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001333 is_cached, value = self._cached_remote_url
1334 if is_cached:
1335 return value
1336
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001337 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001338 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1339
1340 # If URL is pointing to a local directory, it is probably a git cache.
1341 if os.path.isdir(url):
1342 url = RunGit(['config', 'remote.%s.url' % remote],
1343 error_ok=True,
1344 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001345 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001346 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001348 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001349 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001350 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001351 self.issue = self._GitGetBranchConfigValue(
1352 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001353 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354 return self.issue
1355
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 def GetIssueURL(self):
1357 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001358 issue = self.GetIssue()
1359 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001360 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001361 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001363 def GetDescription(self, pretty=False, force=False):
1364 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001366 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367 self.has_description = True
1368 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001369 # Set width to 72 columns + 2 space indent.
1370 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001372 lines = self.description.splitlines()
1373 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 return self.description
1375
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001376 def GetDescriptionFooters(self):
1377 """Returns (non_footer_lines, footers) for the commit message.
1378
1379 Returns:
1380 non_footer_lines (list(str)) - Simple list of description lines without
1381 any footer. The lines do not contain newlines, nor does the list contain
1382 the empty line between the message and the footers.
1383 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1384 [("Change-Id", "Ideadbeef...."), ...]
1385 """
1386 raw_description = self.GetDescription()
1387 msg_lines, _, footers = git_footers.split_footers(raw_description)
1388 if footers:
1389 msg_lines = msg_lines[:len(msg_lines)-1]
1390 return msg_lines, footers
1391
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001393 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001394 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001395 self.patchset = self._GitGetBranchConfigValue(
1396 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001397 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398 return self.patchset
1399
1400 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001401 """Set this branch's patchset. If patchset=0, clears the patchset."""
1402 assert self.GetBranch()
1403 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001404 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001405 else:
1406 self.patchset = int(patchset)
1407 self._GitSetBranchConfigValue(
1408 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001410 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001411 """Set this branch's issue. If issue isn't given, clears the issue."""
1412 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001414 issue = int(issue)
1415 self._GitSetBranchConfigValue(
1416 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001417 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001418 codereview_server = self._codereview_impl.GetCodereviewServer()
1419 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001420 self._GitSetBranchConfigValue(
1421 self._codereview_impl.CodereviewServerConfigKey(),
1422 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423 else:
tandrii5d48c322016-08-18 16:19:37 -07001424 # Reset all of these just to be clean.
1425 reset_suffixes = [
1426 'last-upload-hash',
1427 self._codereview_impl.IssueConfigKey(),
1428 self._codereview_impl.PatchsetConfigKey(),
1429 self._codereview_impl.CodereviewServerConfigKey(),
1430 ] + self._PostUnsetIssueProperties()
1431 for prop in reset_suffixes:
1432 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001433 msg = RunGit(['log', '-1', '--format=%B']).strip()
1434 if msg and git_footers.get_footer_change_id(msg):
1435 print('WARNING: The change patched into this branch has a Change-Id. '
1436 'Removing it.')
1437 RunGit(['commit', '--amend', '-m',
1438 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001439 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001440 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441
dnjba1b0f32016-09-02 12:37:42 -07001442 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001443 if not self.GitSanityChecks(upstream_branch):
1444 DieWithError('\nGit sanity check failure')
1445
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001446 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001447 if not root:
1448 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001449 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001450
1451 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001452 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001453 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001454 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001455 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001456 except subprocess2.CalledProcessError:
1457 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001458 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001459 'This branch probably doesn\'t exist anymore. To reset the\n'
1460 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001461 ' git branch --set-upstream-to origin/master %s\n'
1462 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001463 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001464
maruel@chromium.org52424302012-08-29 15:14:30 +00001465 issue = self.GetIssue()
1466 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001467 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001468 description = self.GetDescription()
1469 else:
1470 # If the change was never uploaded, use the log messages of all commits
1471 # up to the branch point, as git cl upload will prefill the description
1472 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001473 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1474 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001475
1476 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001477 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001478 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001479 name,
1480 description,
1481 absroot,
1482 files,
1483 issue,
1484 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001485 author,
1486 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001487
dsansomee2d6fd92016-09-08 00:10:47 -07001488 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001489 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001490 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001491 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001492
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001493 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1494 """Sets the description for this CL remotely.
1495
1496 You can get description_lines and footers with GetDescriptionFooters.
1497
1498 Args:
1499 description_lines (list(str)) - List of CL description lines without
1500 newline characters.
1501 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1502 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1503 `List-Of-Tokens`). It will be case-normalized so that each token is
1504 title-cased.
1505 """
1506 new_description = '\n'.join(description_lines)
1507 if footers:
1508 new_description += '\n'
1509 for k, v in footers:
1510 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1511 if not git_footers.FOOTER_PATTERN.match(foot):
1512 raise ValueError('Invalid footer %r' % foot)
1513 new_description += foot + '\n'
1514 self.UpdateDescription(new_description, force)
1515
Edward Lesmes8e282792018-04-03 18:50:29 -04001516 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001517 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1518 try:
1519 return presubmit_support.DoPresubmitChecks(change, committing,
1520 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1521 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001522 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1523 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001524 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001525 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001526
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001527 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1528 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001529 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1530 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001531 else:
1532 # Assume url.
1533 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1534 urlparse.urlparse(issue_arg))
1535 if not parsed_issue_arg or not parsed_issue_arg.valid:
1536 DieWithError('Failed to parse issue argument "%s". '
1537 'Must be an issue number or a valid URL.' % issue_arg)
1538 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001539 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001540
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001541 def CMDUpload(self, options, git_diff_args, orig_args):
1542 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001543 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001544 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001545 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001546 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001547 else:
1548 if self.GetBranch() is None:
1549 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1550
1551 # Default to diffing against common ancestor of upstream branch
1552 base_branch = self.GetCommonAncestorWithUpstream()
1553 git_diff_args = [base_branch, 'HEAD']
1554
Aaron Gablec4c40d12017-05-22 11:49:53 -07001555
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001556 # Fast best-effort checks to abort before running potentially
1557 # expensive hooks if uploading is likely to fail anyway. Passing these
1558 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001559 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001560 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001561
1562 # Apply watchlists on upload.
1563 change = self.GetChange(base_branch, None)
1564 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1565 files = [f.LocalPath() for f in change.AffectedFiles()]
1566 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001567 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001568
1569 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001570 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001571 # Set the reviewer list now so that presubmit checks can access it.
1572 change_description = ChangeDescription(change.FullDescriptionText())
1573 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001574 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001575 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001576 change)
1577 change.SetDescriptionText(change_description.description)
1578 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001579 may_prompt=not options.force,
1580 verbose=options.verbose,
1581 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001582 if not hook_results.should_continue():
1583 return 1
1584 if not options.reviewers and hook_results.reviewers:
1585 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001586 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001587
Aaron Gable13101a62018-02-09 13:20:41 -08001588 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001589 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001590 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001591 _git_set_branch_config_value('last-upload-hash',
1592 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001593 # Run post upload hooks, if specified.
1594 if settings.GetRunPostUploadHook():
1595 presubmit_support.DoPostUploadExecuter(
1596 change,
1597 self,
1598 settings.GetRoot(),
1599 options.verbose,
1600 sys.stdout)
1601
1602 # Upload all dependencies if specified.
1603 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001604 print()
1605 print('--dependencies has been specified.')
1606 print('All dependent local branches will be re-uploaded.')
1607 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001608 # Remove the dependencies flag from args so that we do not end up in a
1609 # loop.
1610 orig_args.remove('--dependencies')
1611 ret = upload_branch_deps(self, orig_args)
1612 return ret
1613
Ravi Mistry31e7d562018-04-02 12:53:57 -04001614 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1615 """Sets labels on the change based on the provided flags.
1616
1617 Sets labels if issue is already uploaded and known, else returns without
1618 doing anything.
1619
1620 Args:
1621 enable_auto_submit: Sets Auto-Submit+1 on the change.
1622 use_commit_queue: Sets Commit-Queue+2 on the change.
1623 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1624 both use_commit_queue and cq_dry_run are true.
1625 """
1626 if not self.GetIssue():
1627 return
1628 try:
1629 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1630 cq_dry_run)
1631 return 0
1632 except KeyboardInterrupt:
1633 raise
1634 except:
1635 labels = []
1636 if enable_auto_submit:
1637 labels.append('Auto-Submit')
1638 if use_commit_queue or cq_dry_run:
1639 labels.append('Commit-Queue')
1640 print('WARNING: Failed to set label(s) on your change: %s\n'
1641 'Either:\n'
1642 ' * Your project does not have the above label(s),\n'
1643 ' * You don\'t have permission to set the above label(s),\n'
1644 ' * There\'s a bug in this code (see stack trace below).\n' %
1645 (', '.join(labels)))
1646 # Still raise exception so that stack trace is printed.
1647 raise
1648
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001649 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001650 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001651
1652 Issue must have been already uploaded and known.
1653 """
1654 assert new_state in _CQState.ALL_STATES
1655 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001656 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001657 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001658 return 0
1659 except KeyboardInterrupt:
1660 raise
1661 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001662 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001663 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001664 ' * Your project has no CQ,\n'
1665 ' * You don\'t have permission to change the CQ state,\n'
1666 ' * There\'s a bug in this code (see stack trace below).\n'
1667 'Consider specifying which bots to trigger manually or asking your '
1668 'project owners for permissions or contacting Chrome Infra at:\n'
1669 'https://www.chromium.org/infra\n\n' %
1670 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001671 # Still raise exception so that stack trace is printed.
1672 raise
1673
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001674 # Forward methods to codereview specific implementation.
1675
Aaron Gable636b13f2017-07-14 10:42:48 -07001676 def AddComment(self, message, publish=None):
1677 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001678
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001679 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001680 """Returns list of _CommentSummary for each comment.
1681
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001682 args:
1683 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001684 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001685 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001686
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001687 def CloseIssue(self):
1688 return self._codereview_impl.CloseIssue()
1689
1690 def GetStatus(self):
1691 return self._codereview_impl.GetStatus()
1692
1693 def GetCodereviewServer(self):
1694 return self._codereview_impl.GetCodereviewServer()
1695
tandriide281ae2016-10-12 06:02:30 -07001696 def GetIssueOwner(self):
1697 """Get owner from codereview, which may differ from this checkout."""
1698 return self._codereview_impl.GetIssueOwner()
1699
Edward Lemur707d70b2018-02-07 00:50:14 +01001700 def GetReviewers(self):
1701 return self._codereview_impl.GetReviewers()
1702
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001703 def GetMostRecentPatchset(self):
1704 return self._codereview_impl.GetMostRecentPatchset()
1705
tandriide281ae2016-10-12 06:02:30 -07001706 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001707 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001708 return self._codereview_impl.CannotTriggerTryJobReason()
1709
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001710 def GetTryJobProperties(self, patchset=None):
1711 """Returns dictionary of properties to launch try job."""
1712 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001713
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001714 def __getattr__(self, attr):
1715 # This is because lots of untested code accesses Rietveld-specific stuff
1716 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001717 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001718 # Note that child method defines __getattr__ as well, and forwards it here,
1719 # because _RietveldChangelistImpl is not cleaned up yet, and given
1720 # deprecation of Rietveld, it should probably be just removed.
1721 # Until that time, avoid infinite recursion by bypassing __getattr__
1722 # of implementation class.
1723 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001724
1725
1726class _ChangelistCodereviewBase(object):
1727 """Abstract base class encapsulating codereview specifics of a changelist."""
1728 def __init__(self, changelist):
1729 self._changelist = changelist # instance of Changelist
1730
1731 def __getattr__(self, attr):
1732 # Forward methods to changelist.
1733 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1734 # _RietveldChangelistImpl to avoid this hack?
1735 return getattr(self._changelist, attr)
1736
1737 def GetStatus(self):
1738 """Apply a rough heuristic to give a simple summary of an issue's review
1739 or CQ status, assuming adherence to a common workflow.
1740
1741 Returns None if no issue for this branch, or specific string keywords.
1742 """
1743 raise NotImplementedError()
1744
1745 def GetCodereviewServer(self):
1746 """Returns server URL without end slash, like "https://codereview.com"."""
1747 raise NotImplementedError()
1748
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001749 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001750 """Fetches and returns description from the codereview server."""
1751 raise NotImplementedError()
1752
tandrii5d48c322016-08-18 16:19:37 -07001753 @classmethod
1754 def IssueConfigKey(cls):
1755 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001756 raise NotImplementedError()
1757
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001758 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001759 def PatchsetConfigKey(cls):
1760 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001761 raise NotImplementedError()
1762
tandrii5d48c322016-08-18 16:19:37 -07001763 @classmethod
1764 def CodereviewServerConfigKey(cls):
1765 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001766 raise NotImplementedError()
1767
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001768 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001769 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001770 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001771
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001772 def GetGerritObjForPresubmit(self):
1773 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1774 return None
1775
dsansomee2d6fd92016-09-08 00:10:47 -07001776 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001777 """Update the description on codereview site."""
1778 raise NotImplementedError()
1779
Aaron Gable636b13f2017-07-14 10:42:48 -07001780 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001781 """Posts a comment to the codereview site."""
1782 raise NotImplementedError()
1783
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001784 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001785 raise NotImplementedError()
1786
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001787 def CloseIssue(self):
1788 """Closes the issue."""
1789 raise NotImplementedError()
1790
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001791 def GetMostRecentPatchset(self):
1792 """Returns the most recent patchset number from the codereview site."""
1793 raise NotImplementedError()
1794
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001795 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001796 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001797 """Fetches and applies the issue.
1798
1799 Arguments:
1800 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1801 reject: if True, reject the failed patch instead of switching to 3-way
1802 merge. Rietveld only.
1803 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1804 only.
1805 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001806 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001807 """
1808 raise NotImplementedError()
1809
1810 @staticmethod
1811 def ParseIssueURL(parsed_url):
1812 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1813 failed."""
1814 raise NotImplementedError()
1815
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001816 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001817 """Best effort check that user is authenticated with codereview server.
1818
1819 Arguments:
1820 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001821 refresh: whether to attempt to refresh credentials. Ignored if not
1822 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001823 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001824 raise NotImplementedError()
1825
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001826 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001827 """Best effort check that uploading isn't supposed to fail for predictable
1828 reasons.
1829
1830 This method should raise informative exception if uploading shouldn't
1831 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001832
1833 Arguments:
1834 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001835 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001836 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001837
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001838 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001839 """Uploads a change to codereview."""
1840 raise NotImplementedError()
1841
Ravi Mistry31e7d562018-04-02 12:53:57 -04001842 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1843 """Sets labels on the change based on the provided flags.
1844
1845 Issue must have been already uploaded and known.
1846 """
1847 raise NotImplementedError()
1848
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001849 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001850 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001851
1852 Issue must have been already uploaded and known.
1853 """
1854 raise NotImplementedError()
1855
tandriie113dfd2016-10-11 10:20:12 -07001856 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001857 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001858 raise NotImplementedError()
1859
tandriide281ae2016-10-12 06:02:30 -07001860 def GetIssueOwner(self):
1861 raise NotImplementedError()
1862
Edward Lemur707d70b2018-02-07 00:50:14 +01001863 def GetReviewers(self):
1864 raise NotImplementedError()
1865
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001866 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001867 raise NotImplementedError()
1868
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001869
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001870class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001871 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001872 # auth_config is Rietveld thing, kept here to preserve interface only.
1873 super(_GerritChangelistImpl, self).__init__(changelist)
1874 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001875 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001876 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001877 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001878 # Map from change number (issue) to its detail cache.
1879 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001880
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001881 if codereview_host is not None:
1882 assert not codereview_host.startswith('https://'), codereview_host
1883 self._gerrit_host = codereview_host
1884 self._gerrit_server = 'https://%s' % codereview_host
1885
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001886 def _GetGerritHost(self):
1887 # Lazy load of configs.
1888 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001889 if self._gerrit_host and '.' not in self._gerrit_host:
1890 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1891 # This happens for internal stuff http://crbug.com/614312.
1892 parsed = urlparse.urlparse(self.GetRemoteUrl())
1893 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001894 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07001895 ' Your current remote is: %s' % self.GetRemoteUrl())
1896 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1897 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001898 return self._gerrit_host
1899
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001900 def _GetGitHost(self):
1901 """Returns git host to be used when uploading change to Gerrit."""
1902 return urlparse.urlparse(self.GetRemoteUrl()).netloc
1903
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001904 def GetCodereviewServer(self):
1905 if not self._gerrit_server:
1906 # If we're on a branch then get the server potentially associated
1907 # with that branch.
1908 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001909 self._gerrit_server = self._GitGetBranchConfigValue(
1910 self.CodereviewServerConfigKey())
1911 if self._gerrit_server:
1912 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001913 if not self._gerrit_server:
1914 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1915 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001916 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001917 parts[0] = parts[0] + '-review'
1918 self._gerrit_host = '.'.join(parts)
1919 self._gerrit_server = 'https://%s' % self._gerrit_host
1920 return self._gerrit_server
1921
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001922 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001923 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001924 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001925 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001926 logging.warn('can\'t detect Gerrit project.')
1927 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001928 project = urlparse.urlparse(remote_url).path.strip('/')
1929 if project.endswith('.git'):
1930 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001931 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1932 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1933 # gitiles/git-over-https protocol. E.g.,
1934 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1935 # as
1936 # https://chromium.googlesource.com/v8/v8
1937 if project.startswith('a/'):
1938 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001939 return project
1940
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001941 def _GerritChangeIdentifier(self):
1942 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1943
1944 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001945 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001946 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001947 project = self._GetGerritProject()
1948 if project:
1949 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1950 # Fall back on still unique, but less efficient change number.
1951 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001952
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001953 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001954 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001955 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001956
tandrii5d48c322016-08-18 16:19:37 -07001957 @classmethod
1958 def PatchsetConfigKey(cls):
1959 return 'gerritpatchset'
1960
1961 @classmethod
1962 def CodereviewServerConfigKey(cls):
1963 return 'gerritserver'
1964
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001965 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001966 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001967 if settings.GetGerritSkipEnsureAuthenticated():
1968 # For projects with unusual authentication schemes.
1969 # See http://crbug.com/603378.
1970 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001971
1972 # Check presence of cookies only if using cookies-based auth method.
1973 cookie_auth = gerrit_util.Authenticator.get()
1974 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001975 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001976
1977 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001978 self.GetCodereviewServer()
1979 git_host = self._GetGitHost()
1980 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001981
1982 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1983 git_auth = cookie_auth.get_auth_header(git_host)
1984 if gerrit_auth and git_auth:
1985 if gerrit_auth == git_auth:
1986 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001987 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001988 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001989 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001990 ' %s\n'
1991 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001992 ' Consider running the following command:\n'
1993 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001994 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02001995 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001996 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001997 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001998 cookie_auth.get_new_password_message(git_host)))
1999 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002000 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002001 return
2002 else:
2003 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002004 ([] if gerrit_auth else [self._gerrit_host]) +
2005 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002006 DieWithError('Credentials for the following hosts are required:\n'
2007 ' %s\n'
2008 'These are read from %s (or legacy %s)\n'
2009 '%s' % (
2010 '\n '.join(missing),
2011 cookie_auth.get_gitcookies_path(),
2012 cookie_auth.get_netrc_path(),
2013 cookie_auth.get_new_password_message(git_host)))
2014
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002015 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002016 if not self.GetIssue():
2017 return
2018
2019 # Warm change details cache now to avoid RPCs later, reducing latency for
2020 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002021 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002022 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002023
2024 status = self._GetChangeDetail()['status']
2025 if status in ('MERGED', 'ABANDONED'):
2026 DieWithError('Change %s has been %s, new uploads are not allowed' %
2027 (self.GetIssueURL(),
2028 'submitted' if status == 'MERGED' else 'abandoned'))
2029
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002030 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2031 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2032 # Apparently this check is not very important? Otherwise get_auth_email
2033 # could have been added to other implementations of Authenticator.
2034 cookies_auth = gerrit_util.Authenticator.get()
2035 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002036 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002037
2038 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002039 if self.GetIssueOwner() == cookies_user:
2040 return
2041 logging.debug('change %s owner is %s, cookies user is %s',
2042 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002043 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002044 # so ask what Gerrit thinks of this user.
2045 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2046 if details['email'] == self.GetIssueOwner():
2047 return
2048 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002049 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002050 'as %s.\n'
2051 'Uploading may fail due to lack of permissions.' %
2052 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2053 confirm_or_exit(action='upload')
2054
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002055 def _PostUnsetIssueProperties(self):
2056 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002057 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002058
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002059 def GetGerritObjForPresubmit(self):
2060 return presubmit_support.GerritAccessor(self._GetGerritHost())
2061
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002062 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002063 """Apply a rough heuristic to give a simple summary of an issue's review
2064 or CQ status, assuming adherence to a common workflow.
2065
2066 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002067 * 'error' - error from review tool (including deleted issues)
2068 * 'unsent' - no reviewers added
2069 * 'waiting' - waiting for review
2070 * 'reply' - waiting for uploader to reply to review
2071 * 'lgtm' - Code-Review label has been set
2072 * 'commit' - in the commit queue
2073 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002074 """
2075 if not self.GetIssue():
2076 return None
2077
2078 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002079 data = self._GetChangeDetail([
2080 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002081 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002082 return 'error'
2083
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002084 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002085 return 'closed'
2086
Aaron Gable9ab38c62017-04-06 14:36:33 -07002087 if data['labels'].get('Commit-Queue', {}).get('approved'):
2088 # The section will have an "approved" subsection if anyone has voted
2089 # the maximum value on the label.
2090 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002091
Aaron Gable9ab38c62017-04-06 14:36:33 -07002092 if data['labels'].get('Code-Review', {}).get('approved'):
2093 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002094
2095 if not data.get('reviewers', {}).get('REVIEWER', []):
2096 return 'unsent'
2097
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002098 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002099 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2100 last_message_author = messages.pop().get('author', {})
2101 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002102 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2103 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002104 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002105 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002106 if last_message_author.get('_account_id') == owner:
2107 # Most recent message was by owner.
2108 return 'waiting'
2109 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002110 # Some reply from non-owner.
2111 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002112
2113 # Somehow there are no messages even though there are reviewers.
2114 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002115
2116 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002117 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002118 patchset = data['revisions'][data['current_revision']]['_number']
2119 self.SetPatchset(patchset)
2120 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002121
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002122 def FetchDescription(self, force=False):
2123 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2124 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002125 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002126 return data['revisions'][current_rev]['commit']['message'].encode(
2127 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002128
dsansomee2d6fd92016-09-08 00:10:47 -07002129 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002130 if gerrit_util.HasPendingChangeEdit(
2131 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002132 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002133 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002134 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002135 'unpublished edit. Either publish the edit in the Gerrit web UI '
2136 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002137
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002138 gerrit_util.DeletePendingChangeEdit(
2139 self._GetGerritHost(), self._GerritChangeIdentifier())
2140 gerrit_util.SetCommitMessage(
2141 self._GetGerritHost(), self._GerritChangeIdentifier(),
2142 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002143
Aaron Gable636b13f2017-07-14 10:42:48 -07002144 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002145 gerrit_util.SetReview(
2146 self._GetGerritHost(), self._GerritChangeIdentifier(),
2147 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002148
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002149 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002150 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002151 messages = self._GetChangeDetail(
2152 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2153 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002154 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002155
2156 # Build dictionary of file comments for easy access and sorting later.
2157 # {author+date: {path: {patchset: {line: url+message}}}}
2158 comments = collections.defaultdict(
2159 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2160 for path, line_comments in file_comments.iteritems():
2161 for comment in line_comments:
2162 if comment.get('tag', '').startswith('autogenerated'):
2163 continue
2164 key = (comment['author']['email'], comment['updated'])
2165 if comment.get('side', 'REVISION') == 'PARENT':
2166 patchset = 'Base'
2167 else:
2168 patchset = 'PS%d' % comment['patch_set']
2169 line = comment.get('line', 0)
2170 url = ('https://%s/c/%s/%s/%s#%s%s' %
2171 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2172 'b' if comment.get('side') == 'PARENT' else '',
2173 str(line) if line else ''))
2174 comments[key][path][patchset][line] = (url, comment['message'])
2175
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002176 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002177 for msg in messages:
2178 # Don't bother showing autogenerated messages.
2179 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2180 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002181 # Gerrit spits out nanoseconds.
2182 assert len(msg['date'].split('.')[-1]) == 9
2183 date = datetime.datetime.strptime(msg['date'][:-3],
2184 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002185 message = msg['message']
2186 key = (msg['author']['email'], msg['date'])
2187 if key in comments:
2188 message += '\n'
2189 for path, patchsets in sorted(comments.get(key, {}).items()):
2190 if readable:
2191 message += '\n%s' % path
2192 for patchset, lines in sorted(patchsets.items()):
2193 for line, (url, content) in sorted(lines.items()):
2194 if line:
2195 line_str = 'Line %d' % line
2196 path_str = '%s:%d:' % (path, line)
2197 else:
2198 line_str = 'File comment'
2199 path_str = '%s:0:' % path
2200 if readable:
2201 message += '\n %s, %s: %s' % (patchset, line_str, url)
2202 message += '\n %s\n' % content
2203 else:
2204 message += '\n%s ' % path_str
2205 message += '\n%s\n' % content
2206
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002207 summary.append(_CommentSummary(
2208 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002209 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002210 sender=msg['author']['email'],
2211 # These could be inferred from the text messages and correlated with
2212 # Code-Review label maximum, however this is not reliable.
2213 # Leaving as is until the need arises.
2214 approval=False,
2215 disapproval=False,
2216 ))
2217 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002218
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002219 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002220 gerrit_util.AbandonChange(
2221 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002222
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002223 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002224 gerrit_util.SubmitChange(
2225 self._GetGerritHost(), self._GerritChangeIdentifier(),
2226 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002227
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002228 def _GetChangeDetail(self, options=None, no_cache=False):
2229 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002230
2231 If fresh data is needed, set no_cache=True which will clear cache and
2232 thus new data will be fetched from Gerrit.
2233 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002234 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002235 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002236
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002237 # Optimization to avoid multiple RPCs:
2238 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2239 'CURRENT_COMMIT' not in options):
2240 options.append('CURRENT_COMMIT')
2241
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002242 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002243 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002244 options = [o.upper() for o in options]
2245
2246 # Check in cache first unless no_cache is True.
2247 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002248 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002249 else:
2250 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002251 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002252 # Assumption: data fetched before with extra options is suitable
2253 # for return for a smaller set of options.
2254 # For example, if we cached data for
2255 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2256 # and request is for options=[CURRENT_REVISION],
2257 # THEN we can return prior cached data.
2258 if options_set.issubset(cached_options_set):
2259 return data
2260
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002261 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002262 data = gerrit_util.GetChangeDetail(
2263 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002264 except gerrit_util.GerritError as e:
2265 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002266 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002267 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002268
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002269 self._detail_cache.setdefault(cache_key, []).append(
2270 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002271 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002272
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002273 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002274 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002275 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002276 data = gerrit_util.GetChangeCommit(
2277 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002278 except gerrit_util.GerritError as e:
2279 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002280 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002281 raise
agable32978d92016-11-01 12:55:02 -07002282 return data
2283
Olivier Robin75ee7252018-04-13 10:02:56 +02002284 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002285 if git_common.is_dirty_git_tree('land'):
2286 return 1
tandriid60367b2016-06-22 05:25:12 -07002287 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2288 if u'Commit-Queue' in detail.get('labels', {}):
2289 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002290 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2291 'which can test and land changes for you. '
2292 'Are you sure you wish to bypass it?\n',
2293 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002294
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002295 differs = True
tandriic4344b52016-08-29 06:04:54 -07002296 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002297 # Note: git diff outputs nothing if there is no diff.
2298 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002299 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002300 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002301 if detail['current_revision'] == last_upload:
2302 differs = False
2303 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002304 print('WARNING: Local branch contents differ from latest uploaded '
2305 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002306 if differs:
2307 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002308 confirm_or_exit(
2309 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2310 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002311 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002312 elif not bypass_hooks:
2313 hook_results = self.RunHook(
2314 committing=True,
2315 may_prompt=not force,
2316 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002317 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2318 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002319 if not hook_results.should_continue():
2320 return 1
2321
2322 self.SubmitIssue(wait_for_merge=True)
2323 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002324 links = self._GetChangeCommit().get('web_links', [])
2325 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002326 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002327 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002328 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002329 return 0
2330
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002331 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002332 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002333 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002334 assert not directory
2335 assert parsed_issue_arg.valid
2336
2337 self._changelist.issue = parsed_issue_arg.issue
2338
2339 if parsed_issue_arg.hostname:
2340 self._gerrit_host = parsed_issue_arg.hostname
2341 self._gerrit_server = 'https://%s' % self._gerrit_host
2342
tandriic2405f52016-10-10 08:13:15 -07002343 try:
2344 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002345 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002346 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002347
2348 if not parsed_issue_arg.patchset:
2349 # Use current revision by default.
2350 revision_info = detail['revisions'][detail['current_revision']]
2351 patchset = int(revision_info['_number'])
2352 else:
2353 patchset = parsed_issue_arg.patchset
2354 for revision_info in detail['revisions'].itervalues():
2355 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2356 break
2357 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002358 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002359 (parsed_issue_arg.patchset, self.GetIssue()))
2360
Aaron Gable697a91b2018-01-19 15:20:15 -08002361 remote_url = self._changelist.GetRemoteUrl()
2362 if remote_url.endswith('.git'):
2363 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002364 remote_url = remote_url.rstrip('/')
2365
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002366 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002367 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002368
2369 if remote_url != fetch_info['url']:
2370 DieWithError('Trying to patch a change from %s but this repo appears '
2371 'to be %s.' % (fetch_info['url'], remote_url))
2372
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002373 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002374
Aaron Gable62619a32017-06-16 08:22:09 -07002375 if force:
2376 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2377 print('Checked out commit for change %i patchset %i locally' %
2378 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002379 elif nocommit:
2380 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2381 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002382 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002383 RunGit(['cherry-pick', 'FETCH_HEAD'])
2384 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002385 (parsed_issue_arg.issue, patchset))
2386 print('Note: this created a local commit which does not have '
2387 'the same hash as the one uploaded for review. This will make '
2388 'uploading changes based on top of this branch difficult.\n'
2389 'If you want to do that, use "git cl patch --force" instead.')
2390
Stefan Zagerd08043c2017-10-12 12:07:02 -07002391 if self.GetBranch():
2392 self.SetIssue(parsed_issue_arg.issue)
2393 self.SetPatchset(patchset)
2394 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2395 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2396 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2397 else:
2398 print('WARNING: You are in detached HEAD state.\n'
2399 'The patch has been applied to your checkout, but you will not be '
2400 'able to upload a new patch set to the gerrit issue.\n'
2401 'Try using the \'-b\' option if you would like to work on a '
2402 'branch and/or upload a new patch set.')
2403
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002404 return 0
2405
2406 @staticmethod
2407 def ParseIssueURL(parsed_url):
2408 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2409 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002410 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2411 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002412 # Short urls like https://domain/<issue_number> can be used, but don't allow
2413 # specifying the patchset (you'd 404), but we allow that here.
2414 if parsed_url.path == '/':
2415 part = parsed_url.fragment
2416 else:
2417 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002418 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002419 if match:
2420 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002421 issue=int(match.group(3)),
2422 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002423 hostname=parsed_url.netloc,
2424 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002425 return None
2426
tandrii16e0b4e2016-06-07 10:34:28 -07002427 def _GerritCommitMsgHookCheck(self, offer_removal):
2428 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2429 if not os.path.exists(hook):
2430 return
2431 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2432 # custom developer made one.
2433 data = gclient_utils.FileRead(hook)
2434 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2435 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002436 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002437 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002438 'and may interfere with it in subtle ways.\n'
2439 'We recommend you remove the commit-msg hook.')
2440 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002441 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002442 gclient_utils.rm_file_or_tree(hook)
2443 print('Gerrit commit-msg hook removed.')
2444 else:
2445 print('OK, will keep Gerrit commit-msg hook in place.')
2446
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002447 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002448 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002449 if options.squash and options.no_squash:
2450 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002451
2452 if not options.squash and not options.no_squash:
2453 # Load default for user, repo, squash=true, in this order.
2454 options.squash = settings.GetSquashGerritUploads()
2455 elif options.no_squash:
2456 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002457
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002458 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002459 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002460
Aaron Gableb56ad332017-01-06 15:24:31 -08002461 # This may be None; default fallback value is determined in logic below.
2462 title = options.title
2463
Dominic Battre7d1c4842017-10-27 09:17:28 +02002464 # Extract bug number from branch name.
2465 bug = options.bug
2466 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2467 if not bug and match:
2468 bug = match.group(1)
2469
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002470 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002471 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002472 if self.GetIssue():
2473 # Try to get the message from a previous upload.
2474 message = self.GetDescription()
2475 if not message:
2476 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002477 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002478 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002479 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002480 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002481 # When uploading a subsequent patchset, -m|--message is taken
2482 # as the patchset title if --title was not provided.
2483 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002484 else:
2485 default_title = RunGit(
2486 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002487 if options.force:
2488 title = default_title
2489 else:
2490 title = ask_for_data(
2491 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002492 change_id = self._GetChangeDetail()['change_id']
2493 while True:
2494 footer_change_ids = git_footers.get_footer_change_id(message)
2495 if footer_change_ids == [change_id]:
2496 break
2497 if not footer_change_ids:
2498 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002499 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002500 continue
2501 # There is already a valid footer but with different or several ids.
2502 # Doing this automatically is non-trivial as we don't want to lose
2503 # existing other footers, yet we want to append just 1 desired
2504 # Change-Id. Thus, just create a new footer, but let user verify the
2505 # new description.
2506 message = '%s\n\nChange-Id: %s' % (message, change_id)
2507 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002508 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002509 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002510 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002511 'Please, check the proposed correction to the description, '
2512 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2513 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2514 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002515 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002516 if not options.force:
2517 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002518 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002519 message = change_desc.description
2520 if not message:
2521 DieWithError("Description is empty. Aborting...")
2522 # Continue the while loop.
2523 # Sanity check of this code - we should end up with proper message
2524 # footer.
2525 assert [change_id] == git_footers.get_footer_change_id(message)
2526 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002527 else: # if not self.GetIssue()
2528 if options.message:
2529 message = options.message
2530 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002531 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002532 if options.title:
2533 message = options.title + '\n\n' + message
2534 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002535
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002536 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002537 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002538 # On first upload, patchset title is always this string, while
2539 # --title flag gets converted to first line of message.
2540 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002541 if not change_desc.description:
2542 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002543 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002544 if len(change_ids) > 1:
2545 DieWithError('too many Change-Id footers, at most 1 allowed.')
2546 if not change_ids:
2547 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002548 change_desc.set_description(git_footers.add_footer_change_id(
2549 change_desc.description,
2550 GenerateGerritChangeId(change_desc.description)))
2551 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002552 assert len(change_ids) == 1
2553 change_id = change_ids[0]
2554
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002555 if options.reviewers or options.tbrs or options.add_owners_to:
2556 change_desc.update_reviewers(options.reviewers, options.tbrs,
2557 options.add_owners_to, change)
2558
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002559 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002560 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2561 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002562 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002563 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2564 desc_tempfile.write(change_desc.description)
2565 desc_tempfile.close()
2566 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2567 '-F', desc_tempfile.name]).strip()
2568 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002569 else:
2570 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002571 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002572 if not change_desc.description:
2573 DieWithError("Description is empty. Aborting...")
2574
2575 if not git_footers.get_footer_change_id(change_desc.description):
2576 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002577 change_desc.set_description(
2578 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002579 if options.reviewers or options.tbrs or options.add_owners_to:
2580 change_desc.update_reviewers(options.reviewers, options.tbrs,
2581 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002582 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002583 # For no-squash mode, we assume the remote called "origin" is the one we
2584 # want. It is not worthwhile to support different workflows for
2585 # no-squash mode.
2586 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002587 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2588
2589 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002590 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002591 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2592 ref_to_push)]).splitlines()
2593 if len(commits) > 1:
2594 print('WARNING: This will upload %d commits. Run the following command '
2595 'to see which commits will be uploaded: ' % len(commits))
2596 print('git log %s..%s' % (parent, ref_to_push))
2597 print('You can also use `git squash-branch` to squash these into a '
2598 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002599 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002600
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002601 if options.reviewers or options.tbrs or options.add_owners_to:
2602 change_desc.update_reviewers(options.reviewers, options.tbrs,
2603 options.add_owners_to, change)
2604
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002605 reviewers = sorted(change_desc.get_reviewers())
2606 # Add cc's from the CC_LIST and --cc flag (if any).
2607 if not options.private and not options.no_autocc:
2608 cc = self.GetCCList().split(',')
2609 else:
2610 cc = []
2611 if options.cc:
2612 cc.extend(options.cc)
2613 cc = filter(None, [email.strip() for email in cc])
2614 if change_desc.get_cced():
2615 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002616 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2617 valid_accounts = set(reviewers + cc)
2618 # TODO(crbug/877717): relax this for all hosts.
2619 else:
2620 valid_accounts = gerrit_util.ValidAccounts(
2621 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002622 logging.info('accounts %s are recognized, %s invalid',
2623 sorted(valid_accounts),
2624 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002625
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002626 # Extra options that can be specified at push time. Doc:
2627 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002628 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002629
Aaron Gable844cf292017-06-28 11:32:59 -07002630 # By default, new changes are started in WIP mode, and subsequent patchsets
2631 # don't send email. At any time, passing --send-mail will mark the change
2632 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002633 if options.send_mail:
2634 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002635 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002636 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002637 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002638 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002639 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002640
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002641 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002642 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002643
Aaron Gable9b713dd2016-12-14 16:04:21 -08002644 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002645 # Punctuation and whitespace in |title| must be percent-encoded.
2646 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002647
agablec6787972016-09-09 16:13:34 -07002648 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002649 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002650
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002651 for r in sorted(reviewers):
2652 if r in valid_accounts:
2653 refspec_opts.append('r=%s' % r)
2654 reviewers.remove(r)
2655 else:
2656 # TODO(tandrii): this should probably be a hard failure.
2657 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2658 % r)
2659 for c in sorted(cc):
2660 # refspec option will be rejected if cc doesn't correspond to an
2661 # account, even though REST call to add such arbitrary cc may succeed.
2662 if c in valid_accounts:
2663 refspec_opts.append('cc=%s' % c)
2664 cc.remove(c)
2665
rmistry9eadede2016-09-19 11:22:43 -07002666 if options.topic:
2667 # Documentation on Gerrit topics is here:
2668 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002669 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002670
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002671 if not change_desc.get_reviewers(tbr_only=True):
2672 # Change is not TBR, so we can inline setting other labels, too.
2673 # TODO(crbug.com/877717): make this working for TBR, too, by figuring out
2674 # max score for CR label somehow.
2675 if options.enable_auto_submit:
2676 refspec_opts.append('l=Auto-Submit+1')
2677 if options.use_commit_queue:
2678 refspec_opts.append('l=Commit-Queue+2')
2679 elif options.cq_dry_run:
2680 refspec_opts.append('l=Commit-Queue+1')
2681
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002682 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002683 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002684 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002685 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002686 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2687
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002688 refspec_suffix = ''
2689 if refspec_opts:
2690 refspec_suffix = '%' + ','.join(refspec_opts)
2691 assert ' ' not in refspec_suffix, (
2692 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2693 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2694
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002695 try:
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002696 before_push = time_time()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002697 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00002698 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemuredcefdc2018-11-08 14:41:42 +00002699 print_stdout=True,
Edward Lemur49c8eaf2018-11-07 22:13:12 +00002700 # Flush after every line: useful for seeing progress when running as
2701 # recipe.
2702 filter_fn=lambda _: sys.stdout.flush())
2703 push_returncode = 0
Edward Lemurfec80c42018-11-01 23:14:14 +00002704 except subprocess2.CalledProcessError as e:
2705 push_returncode = e.returncode
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002706 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002707 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002708 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002709 'credential problems:\n'
2710 ' git cl creds-check\n',
2711 change_desc)
Edward Lemurfec80c42018-11-01 23:14:14 +00002712 finally:
2713 metrics.collector.add_repeated('sub_commands', {
2714 'command': 'git push',
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002715 'execution_time': time_time() - before_push,
Edward Lemurfec80c42018-11-01 23:14:14 +00002716 'exit_code': push_returncode,
2717 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2718 })
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002719
2720 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002721 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002722 change_numbers = [m.group(1)
2723 for m in map(regex.match, push_stdout.splitlines())
2724 if m]
2725 if len(change_numbers) != 1:
2726 DieWithError(
2727 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002728 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002729 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002730 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002731
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002732 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002733 # GetIssue() is not set in case of non-squash uploads according to tests.
2734 # TODO(agable): non-squash uploads in git cl should be removed.
2735 gerrit_util.AddReviewers(
2736 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002737 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002738 reviewers, cc,
2739 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002740
Aaron Gablefd238082017-06-07 13:42:34 -07002741 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09002742 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
2743 score = 1
2744 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
2745 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
2746 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07002747 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002748 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002749 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09002750 msg='Self-approving for TBR',
2751 labels={'Code-Review': score})
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002752 # Labels aren't set through refspec only if tbr is set (see check above).
2753 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
2754 options.cq_dry_run)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002755 return 0
2756
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002757 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2758 change_desc):
2759 """Computes parent of the generated commit to be uploaded to Gerrit.
2760
2761 Returns revision or a ref name.
2762 """
2763 if custom_cl_base:
2764 # Try to avoid creating additional unintended CLs when uploading, unless
2765 # user wants to take this risk.
2766 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2767 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2768 local_ref_of_target_remote])
2769 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002770 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002771 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2772 'If you proceed with upload, more than 1 CL may be created by '
2773 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2774 'If you are certain that specified base `%s` has already been '
2775 'uploaded to Gerrit as another CL, you may proceed.\n' %
2776 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2777 if not force:
2778 confirm_or_exit(
2779 'Do you take responsibility for cleaning up potential mess '
2780 'resulting from proceeding with upload?',
2781 action='upload')
2782 return custom_cl_base
2783
Aaron Gablef97e33d2017-03-30 15:44:27 -07002784 if remote != '.':
2785 return self.GetCommonAncestorWithUpstream()
2786
2787 # If our upstream branch is local, we base our squashed commit on its
2788 # squashed version.
2789 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2790
Aaron Gablef97e33d2017-03-30 15:44:27 -07002791 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002792 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002793
2794 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002795 # TODO(tandrii): consider checking parent change in Gerrit and using its
2796 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2797 # the tree hash of the parent branch. The upside is less likely bogus
2798 # requests to reupload parent change just because it's uploadhash is
2799 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002800 parent = RunGit(['config',
2801 'branch.%s.gerritsquashhash' % upstream_branch_name],
2802 error_ok=True).strip()
2803 # Verify that the upstream branch has been uploaded too, otherwise
2804 # Gerrit will create additional CLs when uploading.
2805 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2806 RunGitSilent(['rev-parse', parent + ':'])):
2807 DieWithError(
2808 '\nUpload upstream branch %s first.\n'
2809 'It is likely that this branch has been rebased since its last '
2810 'upload, so you just need to upload it again.\n'
2811 '(If you uploaded it with --no-squash, then branch dependencies '
2812 'are not supported, and you should reupload with --squash.)'
2813 % upstream_branch_name,
2814 change_desc)
2815 return parent
2816
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002817 def _AddChangeIdToCommitMessage(self, options, args):
2818 """Re-commits using the current message, assumes the commit hook is in
2819 place.
2820 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002821 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002822 git_command = ['commit', '--amend', '-m', log_desc]
2823 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002824 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002825 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002826 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002827 return new_log_desc
2828 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002829 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002830
Ravi Mistry31e7d562018-04-02 12:53:57 -04002831 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2832 """Sets labels on the change based on the provided flags."""
2833 labels = {}
2834 notify = None;
2835 if enable_auto_submit:
2836 labels['Auto-Submit'] = 1
2837 if use_commit_queue:
2838 labels['Commit-Queue'] = 2
2839 elif cq_dry_run:
2840 labels['Commit-Queue'] = 1
2841 notify = False
2842 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002843 gerrit_util.SetReview(
2844 self._GetGerritHost(),
2845 self._GerritChangeIdentifier(),
2846 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04002847
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002848 def SetCQState(self, new_state):
2849 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002850 vote_map = {
2851 _CQState.NONE: 0,
2852 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002853 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002854 }
Aaron Gablefc62f762017-07-17 11:12:07 -07002855 labels = {'Commit-Queue': vote_map[new_state]}
2856 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002857 gerrit_util.SetReview(
2858 self._GetGerritHost(), self._GerritChangeIdentifier(),
2859 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002860
tandriie113dfd2016-10-11 10:20:12 -07002861 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002862 try:
2863 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002864 except GerritChangeNotExists:
2865 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002866
2867 if data['status'] in ('ABANDONED', 'MERGED'):
2868 return 'CL %s is closed' % self.GetIssue()
2869
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002870 def GetTryJobProperties(self, patchset=None):
2871 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002872 data = self._GetChangeDetail(['ALL_REVISIONS'])
2873 patchset = int(patchset or self.GetPatchset())
2874 assert patchset
2875 revision_data = None # Pylint wants it to be defined.
2876 for revision_data in data['revisions'].itervalues():
2877 if int(revision_data['_number']) == patchset:
2878 break
2879 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002880 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002881 (patchset, self.GetIssue()))
2882 return {
2883 'patch_issue': self.GetIssue(),
2884 'patch_set': patchset or self.GetPatchset(),
2885 'patch_project': data['project'],
2886 'patch_storage': 'gerrit',
2887 'patch_ref': revision_data['fetch']['http']['ref'],
2888 'patch_repository_url': revision_data['fetch']['http']['url'],
2889 'patch_gerrit_url': self.GetCodereviewServer(),
2890 }
tandriie113dfd2016-10-11 10:20:12 -07002891
tandriide281ae2016-10-12 06:02:30 -07002892 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002893 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002894
Edward Lemur707d70b2018-02-07 00:50:14 +01002895 def GetReviewers(self):
2896 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002897 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002898
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002899
2900_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002901 'gerrit': _GerritChangelistImpl,
2902}
2903
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002904
iannuccie53c9352016-08-17 14:40:40 -07002905def _add_codereview_issue_select_options(parser, extra=""):
2906 _add_codereview_select_options(parser)
2907
2908 text = ('Operate on this issue number instead of the current branch\'s '
2909 'implicit issue.')
2910 if extra:
2911 text += ' '+extra
2912 parser.add_option('-i', '--issue', type=int, help=text)
2913
2914
2915def _process_codereview_issue_select_options(parser, options):
2916 _process_codereview_select_options(parser, options)
2917 if options.issue is not None and not options.forced_codereview:
2918 parser.error('--issue must be specified with either --rietveld or --gerrit')
2919
2920
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002921def _add_codereview_select_options(parser):
2922 """Appends --gerrit and --rietveld options to force specific codereview."""
2923 parser.codereview_group = optparse.OptionGroup(
2924 parser, 'EXPERIMENTAL! Codereview override options')
2925 parser.add_option_group(parser.codereview_group)
2926 parser.codereview_group.add_option(
2927 '--gerrit', action='store_true',
2928 help='Force the use of Gerrit for codereview')
2929 parser.codereview_group.add_option(
2930 '--rietveld', action='store_true',
2931 help='Force the use of Rietveld for codereview')
2932
2933
2934def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00002935 if options.rietveld:
2936 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002937 options.forced_codereview = None
2938 if options.gerrit:
2939 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002940
2941
tandriif9aefb72016-07-01 09:06:51 -07002942def _get_bug_line_values(default_project, bugs):
2943 """Given default_project and comma separated list of bugs, yields bug line
2944 values.
2945
2946 Each bug can be either:
2947 * a number, which is combined with default_project
2948 * string, which is left as is.
2949
2950 This function may produce more than one line, because bugdroid expects one
2951 project per line.
2952
2953 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2954 ['v8:123', 'chromium:789']
2955 """
2956 default_bugs = []
2957 others = []
2958 for bug in bugs.split(','):
2959 bug = bug.strip()
2960 if bug:
2961 try:
2962 default_bugs.append(int(bug))
2963 except ValueError:
2964 others.append(bug)
2965
2966 if default_bugs:
2967 default_bugs = ','.join(map(str, default_bugs))
2968 if default_project:
2969 yield '%s:%s' % (default_project, default_bugs)
2970 else:
2971 yield default_bugs
2972 for other in sorted(others):
2973 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2974 yield other
2975
2976
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002977class ChangeDescription(object):
2978 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002979 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002980 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002981 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002982 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002983 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2984 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
2985 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
2986 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002987
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002988 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002989 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002990
agable@chromium.org42c20792013-09-12 17:34:49 +00002991 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002992 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002993 return '\n'.join(self._description_lines)
2994
2995 def set_description(self, desc):
2996 if isinstance(desc, basestring):
2997 lines = desc.splitlines()
2998 else:
2999 lines = [line.rstrip() for line in desc]
3000 while lines and not lines[0]:
3001 lines.pop(0)
3002 while lines and not lines[-1]:
3003 lines.pop(-1)
3004 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003005
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003006 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3007 """Rewrites the R=/TBR= line(s) as a single line each.
3008
3009 Args:
3010 reviewers (list(str)) - list of additional emails to use for reviewers.
3011 tbrs (list(str)) - list of additional emails to use for TBRs.
3012 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3013 the change that are missing OWNER coverage. If this is not None, you
3014 must also pass a value for `change`.
3015 change (Change) - The Change that should be used for OWNERS lookups.
3016 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003017 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003018 assert isinstance(tbrs, list), tbrs
3019
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003020 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003021 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003022
3023 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003024 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003025
3026 reviewers = set(reviewers)
3027 tbrs = set(tbrs)
3028 LOOKUP = {
3029 'TBR': tbrs,
3030 'R': reviewers,
3031 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003032
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003033 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003034 regexp = re.compile(self.R_LINE)
3035 matches = [regexp.match(line) for line in self._description_lines]
3036 new_desc = [l for i, l in enumerate(self._description_lines)
3037 if not matches[i]]
3038 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003039
agable@chromium.org42c20792013-09-12 17:34:49 +00003040 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003041
3042 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003043 for match in matches:
3044 if not match:
3045 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003046 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3047
3048 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003049 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003050 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003051 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003052 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003053 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003054 LOOKUP[add_owners_to].update(
3055 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003056
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003057 # If any folks ended up in both groups, remove them from tbrs.
3058 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003059
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003060 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3061 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003062
3063 # Put the new lines in the description where the old first R= line was.
3064 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3065 if 0 <= line_loc < len(self._description_lines):
3066 if new_tbr_line:
3067 self._description_lines.insert(line_loc, new_tbr_line)
3068 if new_r_line:
3069 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003070 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003071 if new_r_line:
3072 self.append_footer(new_r_line)
3073 if new_tbr_line:
3074 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003075
Aaron Gable3a16ed12017-03-23 10:51:55 -07003076 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003077 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003078 self.set_description([
3079 '# Enter a description of the change.',
3080 '# This will be displayed on the codereview site.',
3081 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003082 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003083 '--------------------',
3084 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003085
agable@chromium.org42c20792013-09-12 17:34:49 +00003086 regexp = re.compile(self.BUG_LINE)
3087 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003088 prefix = settings.GetBugPrefix()
3089 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003090 if git_footer:
3091 self.append_footer('Bug: %s' % ', '.join(values))
3092 else:
3093 for value in values:
3094 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003095
agable@chromium.org42c20792013-09-12 17:34:49 +00003096 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003097 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003098 if not content:
3099 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003100 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003101
Bruce Dawson2377b012018-01-11 16:46:49 -08003102 # Strip off comments and default inserted "Bug:" line.
3103 clean_lines = [line.rstrip() for line in lines if not
3104 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003105 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003106 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003107 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003108
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003109 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003110 """Adds a footer line to the description.
3111
3112 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3113 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3114 that Gerrit footers are always at the end.
3115 """
3116 parsed_footer_line = git_footers.parse_footer(line)
3117 if parsed_footer_line:
3118 # Line is a gerrit footer in the form: Footer-Key: any value.
3119 # Thus, must be appended observing Gerrit footer rules.
3120 self.set_description(
3121 git_footers.add_footer(self.description,
3122 key=parsed_footer_line[0],
3123 value=parsed_footer_line[1]))
3124 return
3125
3126 if not self._description_lines:
3127 self._description_lines.append(line)
3128 return
3129
3130 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3131 if gerrit_footers:
3132 # git_footers.split_footers ensures that there is an empty line before
3133 # actual (gerrit) footers, if any. We have to keep it that way.
3134 assert top_lines and top_lines[-1] == ''
3135 top_lines, separator = top_lines[:-1], top_lines[-1:]
3136 else:
3137 separator = [] # No need for separator if there are no gerrit_footers.
3138
3139 prev_line = top_lines[-1] if top_lines else ''
3140 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3141 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3142 top_lines.append('')
3143 top_lines.append(line)
3144 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003145
tandrii99a72f22016-08-17 14:33:24 -07003146 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003147 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003148 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003149 reviewers = [match.group(2).strip()
3150 for match in matches
3151 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003152 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003153
bradnelsond975b302016-10-23 12:20:23 -07003154 def get_cced(self):
3155 """Retrieves the list of reviewers."""
3156 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3157 cced = [match.group(2).strip() for match in matches if match]
3158 return cleanup_list(cced)
3159
Nodir Turakulov23b82142017-11-16 11:04:25 -08003160 def get_hash_tags(self):
3161 """Extracts and sanitizes a list of Gerrit hashtags."""
3162 subject = (self._description_lines or ('',))[0]
3163 subject = re.sub(
3164 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3165
3166 tags = []
3167 start = 0
3168 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3169 while True:
3170 m = bracket_exp.match(subject, start)
3171 if not m:
3172 break
3173 tags.append(self.sanitize_hash_tag(m.group(1)))
3174 start = m.end()
3175
3176 if not tags:
3177 # Try "Tag: " prefix.
3178 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3179 if m:
3180 tags.append(self.sanitize_hash_tag(m.group(1)))
3181 return tags
3182
3183 @classmethod
3184 def sanitize_hash_tag(cls, tag):
3185 """Returns a sanitized Gerrit hash tag.
3186
3187 A sanitized hashtag can be used as a git push refspec parameter value.
3188 """
3189 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3190
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003191 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3192 """Updates this commit description given the parent.
3193
3194 This is essentially what Gnumbd used to do.
3195 Consult https://goo.gl/WMmpDe for more details.
3196 """
3197 assert parent_msg # No, orphan branch creation isn't supported.
3198 assert parent_hash
3199 assert dest_ref
3200 parent_footer_map = git_footers.parse_footers(parent_msg)
3201 # This will also happily parse svn-position, which GnumbD is no longer
3202 # supporting. While we'd generate correct footers, the verifier plugin
3203 # installed in Gerrit will block such commit (ie git push below will fail).
3204 parent_position = git_footers.get_position(parent_footer_map)
3205
3206 # Cherry-picks may have last line obscuring their prior footers,
3207 # from git_footers perspective. This is also what Gnumbd did.
3208 cp_line = None
3209 if (self._description_lines and
3210 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3211 cp_line = self._description_lines.pop()
3212
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003213 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003214
3215 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3216 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003217 for i, line in enumerate(footer_lines):
3218 k, v = git_footers.parse_footer(line) or (None, None)
3219 if k and k.startswith('Cr-'):
3220 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003221
3222 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003223 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003224 if parent_position[0] == dest_ref:
3225 # Same branch as parent.
3226 number = int(parent_position[1]) + 1
3227 else:
3228 number = 1 # New branch, and extra lineage.
3229 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3230 int(parent_position[1])))
3231
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003232 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3233 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003234
3235 self._description_lines = top_lines
3236 if cp_line:
3237 self._description_lines.append(cp_line)
3238 if self._description_lines[-1] != '':
3239 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003240 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003241
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003242
Aaron Gablea1bab272017-04-11 16:38:18 -07003243def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003244 """Retrieves the reviewers that approved a CL from the issue properties with
3245 messages.
3246
3247 Note that the list may contain reviewers that are not committer, thus are not
3248 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003249
3250 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003251 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003252 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003253 return sorted(
3254 set(
3255 message['sender']
3256 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003257 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003258 )
3259 )
3260
3261
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262def FindCodereviewSettingsFile(filename='codereview.settings'):
3263 """Finds the given file starting in the cwd and going up.
3264
3265 Only looks up to the top of the repository unless an
3266 'inherit-review-settings-ok' file exists in the root of the repository.
3267 """
3268 inherit_ok_file = 'inherit-review-settings-ok'
3269 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003270 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003271 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3272 root = '/'
3273 while True:
3274 if filename in os.listdir(cwd):
3275 if os.path.isfile(os.path.join(cwd, filename)):
3276 return open(os.path.join(cwd, filename))
3277 if cwd == root:
3278 break
3279 cwd = os.path.dirname(cwd)
3280
3281
3282def LoadCodereviewSettingsFromFile(fileobj):
3283 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003284 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003286 def SetProperty(name, setting, unset_error_ok=False):
3287 fullname = 'rietveld.' + name
3288 if setting in keyvals:
3289 RunGit(['config', fullname, keyvals[setting]])
3290 else:
3291 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3292
tandrii48df5812016-10-17 03:55:37 -07003293 if not keyvals.get('GERRIT_HOST', False):
3294 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003295 # Only server setting is required. Other settings can be absent.
3296 # In that case, we ignore errors raised during option deletion attempt.
3297 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3298 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3299 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003300 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003301 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3302 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003303 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3304 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003305
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003306 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003307 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003308
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003309 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003310 RunGit(['config', 'gerrit.squash-uploads',
3311 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003312
tandrii@chromium.org28253532016-04-14 13:46:56 +00003313 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003314 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003315 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3316
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003317 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003318 # should be of the form
3319 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3320 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003321 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3322 keyvals['ORIGIN_URL_CONFIG']])
3323
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003324
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003325def urlretrieve(source, destination):
3326 """urllib is broken for SSL connections via a proxy therefore we
3327 can't use urllib.urlretrieve()."""
3328 with open(destination, 'w') as f:
3329 f.write(urllib2.urlopen(source).read())
3330
3331
ukai@chromium.org712d6102013-11-27 00:52:58 +00003332def hasSheBang(fname):
3333 """Checks fname is a #! script."""
3334 with open(fname) as f:
3335 return f.read(2).startswith('#!')
3336
3337
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003338# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3339def DownloadHooks(*args, **kwargs):
3340 pass
3341
3342
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003343def DownloadGerritHook(force):
3344 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003345
3346 Args:
3347 force: True to update hooks. False to install hooks if not present.
3348 """
3349 if not settings.GetIsGerrit():
3350 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003351 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003352 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3353 if not os.access(dst, os.X_OK):
3354 if os.path.exists(dst):
3355 if not force:
3356 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003357 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003358 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003359 if not hasSheBang(dst):
3360 DieWithError('Not a script: %s\n'
3361 'You need to download from\n%s\n'
3362 'into .git/hooks/commit-msg and '
3363 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003364 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3365 except Exception:
3366 if os.path.exists(dst):
3367 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003368 DieWithError('\nFailed to download hooks.\n'
3369 'You need to download from\n%s\n'
3370 'into .git/hooks/commit-msg and '
3371 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003372
3373
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003374class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003375 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003376
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003377 _GOOGLESOURCE = 'googlesource.com'
3378
3379 def __init__(self):
3380 # Cached list of [host, identity, source], where source is either
3381 # .gitcookies or .netrc.
3382 self._all_hosts = None
3383
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003384 def ensure_configured_gitcookies(self):
3385 """Runs checks and suggests fixes to make git use .gitcookies from default
3386 path."""
3387 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3388 configured_path = RunGitSilent(
3389 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003390 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003391 if configured_path:
3392 self._ensure_default_gitcookies_path(configured_path, default)
3393 else:
3394 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003395
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003396 @staticmethod
3397 def _ensure_default_gitcookies_path(configured_path, default_path):
3398 assert configured_path
3399 if configured_path == default_path:
3400 print('git is already configured to use your .gitcookies from %s' %
3401 configured_path)
3402 return
3403
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003404 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003405 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3406 (configured_path, default_path))
3407
3408 if not os.path.exists(configured_path):
3409 print('However, your configured .gitcookies file is missing.')
3410 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3411 action='reconfigure')
3412 RunGit(['config', '--global', 'http.cookiefile', default_path])
3413 return
3414
3415 if os.path.exists(default_path):
3416 print('WARNING: default .gitcookies file already exists %s' %
3417 default_path)
3418 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3419 default_path)
3420
3421 confirm_or_exit('Move existing .gitcookies to default location?',
3422 action='move')
3423 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003424 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003425 print('Moved and reconfigured git to use .gitcookies from %s' %
3426 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003427
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003428 @staticmethod
3429 def _configure_gitcookies_path(default_path):
3430 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3431 if os.path.exists(netrc_path):
3432 print('You seem to be using outdated .netrc for git credentials: %s' %
3433 netrc_path)
3434 print('This tool will guide you through setting up recommended '
3435 '.gitcookies store for git credentials.\n'
3436 '\n'
3437 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3438 ' git config --global --unset http.cookiefile\n'
3439 ' mv %s %s.backup\n\n' % (default_path, default_path))
3440 confirm_or_exit(action='setup .gitcookies')
3441 RunGit(['config', '--global', 'http.cookiefile', default_path])
3442 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003443
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003444 def get_hosts_with_creds(self, include_netrc=False):
3445 if self._all_hosts is None:
3446 a = gerrit_util.CookiesAuthenticator()
3447 self._all_hosts = [
3448 (h, u, s)
3449 for h, u, s in itertools.chain(
3450 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3451 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3452 )
3453 if h.endswith(self._GOOGLESOURCE)
3454 ]
3455
3456 if include_netrc:
3457 return self._all_hosts
3458 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3459
3460 def print_current_creds(self, include_netrc=False):
3461 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3462 if not hosts:
3463 print('No Git/Gerrit credentials found')
3464 return
3465 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3466 header = [('Host', 'User', 'Which file'),
3467 ['=' * l for l in lengths]]
3468 for row in (header + hosts):
3469 print('\t'.join((('%%+%ds' % l) % s)
3470 for l, s in zip(lengths, row)))
3471
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003472 @staticmethod
3473 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003474 """Parses identity "git-<username>.domain" into <username> and domain."""
3475 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003476 # distinguishable from sub-domains. But we do know typical domains:
3477 if identity.endswith('.chromium.org'):
3478 domain = 'chromium.org'
3479 username = identity[:-len('.chromium.org')]
3480 else:
3481 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003482 if username.startswith('git-'):
3483 username = username[len('git-'):]
3484 return username, domain
3485
3486 def _get_usernames_of_domain(self, domain):
3487 """Returns list of usernames referenced by .gitcookies in a given domain."""
3488 identities_by_domain = {}
3489 for _, identity, _ in self.get_hosts_with_creds():
3490 username, domain = self._parse_identity(identity)
3491 identities_by_domain.setdefault(domain, []).append(username)
3492 return identities_by_domain.get(domain)
3493
3494 def _canonical_git_googlesource_host(self, host):
3495 """Normalizes Gerrit hosts (with '-review') to Git host."""
3496 assert host.endswith(self._GOOGLESOURCE)
3497 # Prefix doesn't include '.' at the end.
3498 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3499 if prefix.endswith('-review'):
3500 prefix = prefix[:-len('-review')]
3501 return prefix + '.' + self._GOOGLESOURCE
3502
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003503 def _canonical_gerrit_googlesource_host(self, host):
3504 git_host = self._canonical_git_googlesource_host(host)
3505 prefix = git_host.split('.', 1)[0]
3506 return prefix + '-review.' + self._GOOGLESOURCE
3507
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003508 def _get_counterpart_host(self, host):
3509 assert host.endswith(self._GOOGLESOURCE)
3510 git = self._canonical_git_googlesource_host(host)
3511 gerrit = self._canonical_gerrit_googlesource_host(git)
3512 return git if gerrit == host else gerrit
3513
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003514 def has_generic_host(self):
3515 """Returns whether generic .googlesource.com has been configured.
3516
3517 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3518 """
3519 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3520 if host == '.' + self._GOOGLESOURCE:
3521 return True
3522 return False
3523
3524 def _get_git_gerrit_identity_pairs(self):
3525 """Returns map from canonic host to pair of identities (Git, Gerrit).
3526
3527 One of identities might be None, meaning not configured.
3528 """
3529 host_to_identity_pairs = {}
3530 for host, identity, _ in self.get_hosts_with_creds():
3531 canonical = self._canonical_git_googlesource_host(host)
3532 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3533 idx = 0 if canonical == host else 1
3534 pair[idx] = identity
3535 return host_to_identity_pairs
3536
3537 def get_partially_configured_hosts(self):
3538 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003539 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3540 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3541 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003542
3543 def get_conflicting_hosts(self):
3544 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003545 host
3546 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003547 if None not in (i1, i2) and i1 != i2)
3548
3549 def get_duplicated_hosts(self):
3550 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3551 return set(host for host, count in counters.iteritems() if count > 1)
3552
3553 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3554 'chromium.googlesource.com': 'chromium.org',
3555 'chrome-internal.googlesource.com': 'google.com',
3556 }
3557
3558 def get_hosts_with_wrong_identities(self):
3559 """Finds hosts which **likely** reference wrong identities.
3560
3561 Note: skips hosts which have conflicting identities for Git and Gerrit.
3562 """
3563 hosts = set()
3564 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3565 pair = self._get_git_gerrit_identity_pairs().get(host)
3566 if pair and pair[0] == pair[1]:
3567 _, domain = self._parse_identity(pair[0])
3568 if domain != expected:
3569 hosts.add(host)
3570 return hosts
3571
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003572 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003573 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003574 hosts = sorted(hosts)
3575 assert hosts
3576 if extra_column_func is None:
3577 extras = [''] * len(hosts)
3578 else:
3579 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003580 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3581 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003582 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003583 lines.append(tmpl % he)
3584 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003585
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003586 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003587 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003588 yield ('.googlesource.com wildcard record detected',
3589 ['Chrome Infrastructure team recommends to list full host names '
3590 'explicitly.'],
3591 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003592
3593 dups = self.get_duplicated_hosts()
3594 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003595 yield ('The following hosts were defined twice',
3596 self._format_hosts(dups),
3597 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003598
3599 partial = self.get_partially_configured_hosts()
3600 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003601 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3602 'These hosts are missing',
3603 self._format_hosts(partial, lambda host: 'but %s defined' %
3604 self._get_counterpart_host(host)),
3605 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003606
3607 conflicting = self.get_conflicting_hosts()
3608 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003609 yield ('The following Git hosts have differing credentials from their '
3610 'Gerrit counterparts',
3611 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3612 tuple(self._get_git_gerrit_identity_pairs()[host])),
3613 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003614
3615 wrong = self.get_hosts_with_wrong_identities()
3616 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003617 yield ('These hosts likely use wrong identity',
3618 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3619 (self._get_git_gerrit_identity_pairs()[host][0],
3620 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3621 wrong)
3622
3623 def find_and_report_problems(self):
3624 """Returns True if there was at least one problem, else False."""
3625 found = False
3626 bad_hosts = set()
3627 for title, sublines, hosts in self._find_problems():
3628 if not found:
3629 found = True
3630 print('\n\n.gitcookies problem report:\n')
3631 bad_hosts.update(hosts or [])
3632 print(' %s%s' % (title , (':' if sublines else '')))
3633 if sublines:
3634 print()
3635 print(' %s' % '\n '.join(sublines))
3636 print()
3637
3638 if bad_hosts:
3639 assert found
3640 print(' You can manually remove corresponding lines in your %s file and '
3641 'visit the following URLs with correct account to generate '
3642 'correct credential lines:\n' %
3643 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3644 print(' %s' % '\n '.join(sorted(set(
3645 gerrit_util.CookiesAuthenticator().get_new_password_url(
3646 self._canonical_git_googlesource_host(host))
3647 for host in bad_hosts
3648 ))))
3649 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003650
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003651
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003652@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003653def CMDcreds_check(parser, args):
3654 """Checks credentials and suggests changes."""
3655 _, _ = parser.parse_args(args)
3656
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003657 # Code below checks .gitcookies. Abort if using something else.
3658 authn = gerrit_util.Authenticator.get()
3659 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3660 if isinstance(authn, gerrit_util.GceAuthenticator):
3661 DieWithError(
3662 'This command is not designed for GCE, are you on a bot?\n'
3663 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3664 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003665 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003666 'This command is not designed for bot environment. It checks '
3667 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003668
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003669 checker = _GitCookiesChecker()
3670 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003671
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003672 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003673 checker.print_current_creds(include_netrc=True)
3674
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003675 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003676 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003677 return 0
3678 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003679
3680
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003681@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003682def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003683 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003684 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3685 branch = ShortBranchName(branchref)
3686 _, args = parser.parse_args(args)
3687 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003688 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003689 return RunGit(['config', 'branch.%s.base-url' % branch],
3690 error_ok=False).strip()
3691 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003692 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003693 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3694 error_ok=False).strip()
3695
3696
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003697def color_for_status(status):
3698 """Maps a Changelist status to color, for CMDstatus and other tools."""
3699 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003700 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003701 'waiting': Fore.BLUE,
3702 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003703 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003704 'lgtm': Fore.GREEN,
3705 'commit': Fore.MAGENTA,
3706 'closed': Fore.CYAN,
3707 'error': Fore.WHITE,
3708 }.get(status, Fore.WHITE)
3709
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003710
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003711def get_cl_statuses(changes, fine_grained, max_processes=None):
3712 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003713
3714 If fine_grained is true, this will fetch CL statuses from the server.
3715 Otherwise, simply indicate if there's a matching url for the given branches.
3716
3717 If max_processes is specified, it is used as the maximum number of processes
3718 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3719 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003720
3721 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003722 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003723 if not changes:
3724 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003725
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003726 if not fine_grained:
3727 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003728 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003729 for cl in changes:
3730 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003731 return
3732
3733 # First, sort out authentication issues.
3734 logging.debug('ensuring credentials exist')
3735 for cl in changes:
3736 cl.EnsureAuthenticated(force=False, refresh=True)
3737
3738 def fetch(cl):
3739 try:
3740 return (cl, cl.GetStatus())
3741 except:
3742 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003743 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003744 raise
3745
3746 threads_count = len(changes)
3747 if max_processes:
3748 threads_count = max(1, min(threads_count, max_processes))
3749 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3750
3751 pool = ThreadPool(threads_count)
3752 fetched_cls = set()
3753 try:
3754 it = pool.imap_unordered(fetch, changes).__iter__()
3755 while True:
3756 try:
3757 cl, status = it.next(timeout=5)
3758 except multiprocessing.TimeoutError:
3759 break
3760 fetched_cls.add(cl)
3761 yield cl, status
3762 finally:
3763 pool.close()
3764
3765 # Add any branches that failed to fetch.
3766 for cl in set(changes) - fetched_cls:
3767 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003768
rmistry@google.com2dd99862015-06-22 12:22:18 +00003769
3770def upload_branch_deps(cl, args):
3771 """Uploads CLs of local branches that are dependents of the current branch.
3772
3773 If the local branch dependency tree looks like:
3774 test1 -> test2.1 -> test3.1
3775 -> test3.2
3776 -> test2.2 -> test3.3
3777
3778 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3779 run on the dependent branches in this order:
3780 test2.1, test3.1, test3.2, test2.2, test3.3
3781
3782 Note: This function does not rebase your local dependent branches. Use it when
3783 you make a change to the parent branch that will not conflict with its
3784 dependent branches, and you would like their dependencies updated in
3785 Rietveld.
3786 """
3787 if git_common.is_dirty_git_tree('upload-branch-deps'):
3788 return 1
3789
3790 root_branch = cl.GetBranch()
3791 if root_branch is None:
3792 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3793 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003794 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003795 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3796 'patchset dependencies without an uploaded CL.')
3797
3798 branches = RunGit(['for-each-ref',
3799 '--format=%(refname:short) %(upstream:short)',
3800 'refs/heads'])
3801 if not branches:
3802 print('No local branches found.')
3803 return 0
3804
3805 # Create a dictionary of all local branches to the branches that are dependent
3806 # on it.
3807 tracked_to_dependents = collections.defaultdict(list)
3808 for b in branches.splitlines():
3809 tokens = b.split()
3810 if len(tokens) == 2:
3811 branch_name, tracked = tokens
3812 tracked_to_dependents[tracked].append(branch_name)
3813
vapiera7fbd5a2016-06-16 09:17:49 -07003814 print()
3815 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003816 dependents = []
3817 def traverse_dependents_preorder(branch, padding=''):
3818 dependents_to_process = tracked_to_dependents.get(branch, [])
3819 padding += ' '
3820 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003821 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003822 dependents.append(dependent)
3823 traverse_dependents_preorder(dependent, padding)
3824 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003825 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003826
3827 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003828 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003829 return 0
3830
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003831 confirm_or_exit('This command will checkout all dependent branches and run '
3832 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003833
rmistry@google.com2dd99862015-06-22 12:22:18 +00003834 # Record all dependents that failed to upload.
3835 failures = {}
3836 # Go through all dependents, checkout the branch and upload.
3837 try:
3838 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003839 print()
3840 print('--------------------------------------')
3841 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003842 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003843 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003844 try:
3845 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003846 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003847 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003848 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003849 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003850 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003851 finally:
3852 # Swap back to the original root branch.
3853 RunGit(['checkout', '-q', root_branch])
3854
vapiera7fbd5a2016-06-16 09:17:49 -07003855 print()
3856 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003857 for dependent_branch in dependents:
3858 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003859 print(' %s : %s' % (dependent_branch, upload_status))
3860 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003861
3862 return 0
3863
3864
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003865@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003866def CMDarchive(parser, args):
3867 """Archives and deletes branches associated with closed changelists."""
3868 parser.add_option(
3869 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003870 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003871 parser.add_option(
3872 '-f', '--force', action='store_true',
3873 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003874 parser.add_option(
3875 '-d', '--dry-run', action='store_true',
3876 help='Skip the branch tagging and removal steps.')
3877 parser.add_option(
3878 '-t', '--notags', action='store_true',
3879 help='Do not tag archived branches. '
3880 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003881
3882 auth.add_auth_options(parser)
3883 options, args = parser.parse_args(args)
3884 if args:
3885 parser.error('Unsupported args: %s' % ' '.join(args))
3886 auth_config = auth.extract_auth_config_from_options(options)
3887
3888 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3889 if not branches:
3890 return 0
3891
vapiera7fbd5a2016-06-16 09:17:49 -07003892 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003893 changes = [Changelist(branchref=b, auth_config=auth_config)
3894 for b in branches.splitlines()]
3895 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3896 statuses = get_cl_statuses(changes,
3897 fine_grained=True,
3898 max_processes=options.maxjobs)
3899 proposal = [(cl.GetBranch(),
3900 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3901 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003902 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003903 proposal.sort()
3904
3905 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003906 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003907 return 0
3908
3909 current_branch = GetCurrentBranch()
3910
vapiera7fbd5a2016-06-16 09:17:49 -07003911 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003912 if options.notags:
3913 for next_item in proposal:
3914 print(' ' + next_item[0])
3915 else:
3916 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3917 for next_item in proposal:
3918 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003919
kmarshall9249e012016-08-23 12:02:16 -07003920 # Quit now on precondition failure or if instructed by the user, either
3921 # via an interactive prompt or by command line flags.
3922 if options.dry_run:
3923 print('\nNo changes were made (dry run).\n')
3924 return 0
3925 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003926 print('You are currently on a branch \'%s\' which is associated with a '
3927 'closed codereview issue, so archive cannot proceed. Please '
3928 'checkout another branch and run this command again.' %
3929 current_branch)
3930 return 1
kmarshall9249e012016-08-23 12:02:16 -07003931 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003932 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3933 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003934 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003935 return 1
3936
3937 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003938 if not options.notags:
3939 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003940 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003941
vapiera7fbd5a2016-06-16 09:17:49 -07003942 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003943
3944 return 0
3945
3946
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003947@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003948def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003949 """Show status of changelists.
3950
3951 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003952 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003953 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003954 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003955 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003956 - Magenta in the commit queue
3957 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003958 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003959
3960 Also see 'git cl comments'.
3961 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003962 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003963 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003964 parser.add_option('-f', '--fast', action='store_true',
3965 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003966 parser.add_option(
3967 '-j', '--maxjobs', action='store', type=int,
3968 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003969
3970 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003971 _add_codereview_issue_select_options(
3972 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003973 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003974 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003975 if args:
3976 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003977 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003978
iannuccie53c9352016-08-17 14:40:40 -07003979 if options.issue is not None and not options.field:
3980 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003981
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003982 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003983 cl = Changelist(auth_config=auth_config, issue=options.issue,
3984 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003985 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003986 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003987 elif options.field == 'id':
3988 issueid = cl.GetIssue()
3989 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003990 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003991 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003992 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003993 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003994 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003995 elif options.field == 'status':
3996 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003997 elif options.field == 'url':
3998 url = cl.GetIssueURL()
3999 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004000 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004001 return 0
4002
4003 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4004 if not branches:
4005 print('No local branch found.')
4006 return 0
4007
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004008 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004009 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004010 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004011 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004012 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004013 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004014 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004015
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004016 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004017 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4018 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4019 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004020 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004021 c, status = output.next()
4022 branch_statuses[c.GetBranch()] = status
4023 status = branch_statuses.pop(branch)
4024 url = cl.GetIssueURL()
4025 if url and (not status or status == 'error'):
4026 # The issue probably doesn't exist anymore.
4027 url += ' (broken)'
4028
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004029 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004030 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004031 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004032 color = ''
4033 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004034 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004035 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004036 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004037 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004038
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004039
4040 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004041 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004042 print('Current branch: %s' % branch)
4043 for cl in changes:
4044 if cl.GetBranch() == branch:
4045 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004046 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004047 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004048 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004049 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004050 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004051 print('Issue description:')
4052 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004053 return 0
4054
4055
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004056def colorize_CMDstatus_doc():
4057 """To be called once in main() to add colors to git cl status help."""
4058 colors = [i for i in dir(Fore) if i[0].isupper()]
4059
4060 def colorize_line(line):
4061 for color in colors:
4062 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004063 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004064 indent = len(line) - len(line.lstrip(' ')) + 1
4065 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4066 return line
4067
4068 lines = CMDstatus.__doc__.splitlines()
4069 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4070
4071
phajdan.jre328cf92016-08-22 04:12:17 -07004072def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004073 if path == '-':
4074 json.dump(contents, sys.stdout)
4075 else:
4076 with open(path, 'w') as f:
4077 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004078
4079
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004080@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004081@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004082def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004083 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004084
4085 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004086 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004087 parser.add_option('-r', '--reverse', action='store_true',
4088 help='Lookup the branch(es) for the specified issues. If '
4089 'no issues are specified, all branches with mapped '
4090 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004091 parser.add_option('--json',
4092 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004093 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004094 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004095 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004096
dnj@chromium.org406c4402015-03-03 17:22:28 +00004097 if options.reverse:
4098 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004099 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004100 # Reverse issue lookup.
4101 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004102
4103 git_config = {}
4104 for config in RunGit(['config', '--get-regexp',
4105 r'branch\..*issue']).splitlines():
4106 name, _space, val = config.partition(' ')
4107 git_config[name] = val
4108
dnj@chromium.org406c4402015-03-03 17:22:28 +00004109 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004110 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4111 config_key = _git_branch_config_key(ShortBranchName(branch),
4112 cls.IssueConfigKey())
4113 issue = git_config.get(config_key)
4114 if issue:
4115 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004116 if not args:
4117 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004118 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004119 for issue in args:
4120 if not issue:
4121 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004122 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004123 print('Branch for issue number %s: %s' % (
4124 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004125 if options.json:
4126 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004127 return 0
4128
4129 if len(args) > 0:
4130 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4131 if not issue.valid:
4132 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4133 'or no argument to list it.\n'
4134 'Maybe you want to run git cl status?')
4135 cl = Changelist(codereview=issue.codereview)
4136 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004137 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004138 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004139 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4140 if options.json:
4141 write_json(options.json, {
4142 'issue': cl.GetIssue(),
4143 'issue_url': cl.GetIssueURL(),
4144 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004145 return 0
4146
4147
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004148@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004149def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004150 """Shows or posts review comments for any changelist."""
4151 parser.add_option('-a', '--add-comment', dest='comment',
4152 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004153 parser.add_option('-p', '--publish', action='store_true',
4154 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004155 parser.add_option('-i', '--issue', dest='issue',
4156 help='review issue id (defaults to current issue). '
4157 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004158 parser.add_option('-m', '--machine-readable', dest='readable',
4159 action='store_false', default=True,
4160 help='output comments in a format compatible with '
4161 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004162 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004163 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004164 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004165 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004166 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004167 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004168 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004169
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004170 issue = None
4171 if options.issue:
4172 try:
4173 issue = int(options.issue)
4174 except ValueError:
4175 DieWithError('A review issue id is expected to be a number')
4176
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004177 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4178
4179 if not cl.IsGerrit():
4180 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004181
4182 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004183 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004184 return 0
4185
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004186 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4187 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004188 for comment in summary:
4189 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004190 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004191 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004192 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004193 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004194 color = Fore.MAGENTA
4195 else:
4196 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004197 print('\n%s%s %s%s\n%s' % (
4198 color,
4199 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4200 comment.sender,
4201 Fore.RESET,
4202 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4203
smut@google.comc85ac942015-09-15 16:34:43 +00004204 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004205 def pre_serialize(c):
4206 dct = c.__dict__.copy()
4207 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4208 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004209 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004210 return 0
4211
4212
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004213@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004214@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004215def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004216 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004217 parser.add_option('-d', '--display', action='store_true',
4218 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004219 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004220 help='New description to set for this issue (- for stdin, '
4221 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004222 parser.add_option('-f', '--force', action='store_true',
4223 help='Delete any unpublished Gerrit edits for this issue '
4224 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004225
4226 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004227 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004228 options, args = parser.parse_args(args)
4229 _process_codereview_select_options(parser, options)
4230
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004231 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004232 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004233 target_issue_arg = ParseIssueNumberArgument(args[0],
4234 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004235 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004236 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004237
martiniss6eda05f2016-06-30 10:18:35 -07004238 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004239 'auth_config': auth.extract_auth_config_from_options(options),
4240 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004241 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004242 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004243 if target_issue_arg:
4244 kwargs['issue'] = target_issue_arg.issue
4245 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004246 if target_issue_arg.codereview and not options.forced_codereview:
4247 detected_codereview_from_url = True
4248 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004249
4250 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004251 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004252 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004253 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004254
4255 if detected_codereview_from_url:
4256 logging.info('canonical issue/change URL: %s (type: %s)\n',
4257 cl.GetIssueURL(), target_issue_arg.codereview)
4258
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004259 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004260
smut@google.com34fb6b12015-07-13 20:03:26 +00004261 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004262 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004263 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004264
4265 if options.new_description:
4266 text = options.new_description
4267 if text == '-':
4268 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004269 elif text == '+':
4270 base_branch = cl.GetCommonAncestorWithUpstream()
4271 change = cl.GetChange(base_branch, None, local_description=True)
4272 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004273
4274 description.set_description(text)
4275 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004276 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004277
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004278 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004279 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004280 return 0
4281
4282
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004283@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004284def CMDlint(parser, args):
4285 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004286 parser.add_option('--filter', action='append', metavar='-x,+y',
4287 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004288 auth.add_auth_options(parser)
4289 options, args = parser.parse_args(args)
4290 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004291
4292 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004293 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004294 try:
4295 import cpplint
4296 import cpplint_chromium
4297 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004298 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004299 return 1
4300
4301 # Change the current working directory before calling lint so that it
4302 # shows the correct base.
4303 previous_cwd = os.getcwd()
4304 os.chdir(settings.GetRoot())
4305 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004306 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004307 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4308 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004309 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004310 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004311 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004312
4313 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004314 command = args + files
4315 if options.filter:
4316 command = ['--filter=' + ','.join(options.filter)] + command
4317 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004318
4319 white_regex = re.compile(settings.GetLintRegex())
4320 black_regex = re.compile(settings.GetLintIgnoreRegex())
4321 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4322 for filename in filenames:
4323 if white_regex.match(filename):
4324 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004325 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004326 else:
4327 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4328 extra_check_functions)
4329 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004330 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004331 finally:
4332 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004333 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004334 if cpplint._cpplint_state.error_count != 0:
4335 return 1
4336 return 0
4337
4338
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004339@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004340def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004341 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004342 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004343 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004344 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004345 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004346 parser.add_option('--all', action='store_true',
4347 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004348 parser.add_option('--parallel', action='store_true',
4349 help='Run all tests specified by input_api.RunTests in all '
4350 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004351 auth.add_auth_options(parser)
4352 options, args = parser.parse_args(args)
4353 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004354
sbc@chromium.org71437c02015-04-09 19:29:40 +00004355 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004356 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004357 return 1
4358
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004359 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004360 if args:
4361 base_branch = args[0]
4362 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004363 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004364 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004365
Aaron Gable8076c282017-11-29 14:39:41 -08004366 if options.all:
4367 base_change = cl.GetChange(base_branch, None)
4368 files = [('M', f) for f in base_change.AllFiles()]
4369 change = presubmit_support.GitChange(
4370 base_change.Name(),
4371 base_change.FullDescriptionText(),
4372 base_change.RepositoryRoot(),
4373 files,
4374 base_change.issue,
4375 base_change.patchset,
4376 base_change.author_email,
4377 base_change._upstream)
4378 else:
4379 change = cl.GetChange(base_branch, None)
4380
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004381 cl.RunHook(
4382 committing=not options.upload,
4383 may_prompt=False,
4384 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004385 change=change,
4386 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004387 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004388
4389
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004390def GenerateGerritChangeId(message):
4391 """Returns Ixxxxxx...xxx change id.
4392
4393 Works the same way as
4394 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4395 but can be called on demand on all platforms.
4396
4397 The basic idea is to generate git hash of a state of the tree, original commit
4398 message, author/committer info and timestamps.
4399 """
4400 lines = []
4401 tree_hash = RunGitSilent(['write-tree'])
4402 lines.append('tree %s' % tree_hash.strip())
4403 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4404 if code == 0:
4405 lines.append('parent %s' % parent.strip())
4406 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4407 lines.append('author %s' % author.strip())
4408 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4409 lines.append('committer %s' % committer.strip())
4410 lines.append('')
4411 # Note: Gerrit's commit-hook actually cleans message of some lines and
4412 # whitespace. This code is not doing this, but it clearly won't decrease
4413 # entropy.
4414 lines.append(message)
4415 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4416 stdin='\n'.join(lines))
4417 return 'I%s' % change_hash.strip()
4418
4419
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004420def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004421 """Computes the remote branch ref to use for the CL.
4422
4423 Args:
4424 remote (str): The git remote for the CL.
4425 remote_branch (str): The git remote branch for the CL.
4426 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004427 """
4428 if not (remote and remote_branch):
4429 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004430
wittman@chromium.org455dc922015-01-26 20:15:50 +00004431 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004432 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004433 # refs, which are then translated into the remote full symbolic refs
4434 # below.
4435 if '/' not in target_branch:
4436 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4437 else:
4438 prefix_replacements = (
4439 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4440 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4441 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4442 )
4443 match = None
4444 for regex, replacement in prefix_replacements:
4445 match = re.search(regex, target_branch)
4446 if match:
4447 remote_branch = target_branch.replace(match.group(0), replacement)
4448 break
4449 if not match:
4450 # This is a branch path but not one we recognize; use as-is.
4451 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004452 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4453 # Handle the refs that need to land in different refs.
4454 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004455
wittman@chromium.org455dc922015-01-26 20:15:50 +00004456 # Create the true path to the remote branch.
4457 # Does the following translation:
4458 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4459 # * refs/remotes/origin/master -> refs/heads/master
4460 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4461 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4462 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4463 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4464 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4465 'refs/heads/')
4466 elif remote_branch.startswith('refs/remotes/branch-heads'):
4467 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004468
wittman@chromium.org455dc922015-01-26 20:15:50 +00004469 return remote_branch
4470
4471
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004472def cleanup_list(l):
4473 """Fixes a list so that comma separated items are put as individual items.
4474
4475 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4476 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4477 """
4478 items = sum((i.split(',') for i in l), [])
4479 stripped_items = (i.strip() for i in items)
4480 return sorted(filter(None, stripped_items))
4481
4482
Aaron Gable4db38df2017-11-03 14:59:07 -07004483@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004484@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004485def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004486 """Uploads the current changelist to codereview.
4487
4488 Can skip dependency patchset uploads for a branch by running:
4489 git config branch.branch_name.skip-deps-uploads True
4490 To unset run:
4491 git config --unset branch.branch_name.skip-deps-uploads
4492 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004493
4494 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4495 a bug number, this bug number is automatically populated in the CL
4496 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004497
4498 If subject contains text in square brackets or has "<text>: " prefix, such
4499 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4500 [git-cl] add support for hashtags
4501 Foo bar: implement foo
4502 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004503 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004504 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4505 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004506 parser.add_option('--bypass-watchlists', action='store_true',
4507 dest='bypass_watchlists',
4508 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004509 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004510 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004511 parser.add_option('--message', '-m', dest='message',
4512 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004513 parser.add_option('-b', '--bug',
4514 help='pre-populate the bug number(s) for this issue. '
4515 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004516 parser.add_option('--message-file', dest='message_file',
4517 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004518 parser.add_option('--title', '-t', dest='title',
4519 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004520 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004521 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004522 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004523 parser.add_option('--tbrs',
4524 action='append', default=[],
4525 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004526 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004527 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004528 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004529 parser.add_option('--hashtag', dest='hashtags',
4530 action='append', default=[],
4531 help=('Gerrit hashtag for new CL; '
4532 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004533 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004534 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004535 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004536 help='tell the commit queue to commit this patchset; '
4537 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004538 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004539 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004540 metavar='TARGET',
4541 help='Apply CL to remote ref TARGET. ' +
4542 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004543 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004544 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004545 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004546 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004547 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004548 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004549 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4550 const='TBR', help='add a set of OWNERS to TBR')
4551 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4552 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004553 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4554 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004555 help='Send the patchset to do a CQ dry run right after '
4556 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004557 parser.add_option('--dependencies', action='store_true',
4558 help='Uploads CLs of all the local branches that depend on '
4559 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004560 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4561 help='Sends your change to the CQ after an approval. Only '
4562 'works on repos that have the Auto-Submit label '
4563 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004564 parser.add_option('--parallel', action='store_true',
4565 help='Run all tests specified by input_api.RunTests in all '
4566 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004567
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004568 parser.add_option('--no-autocc', action='store_true',
4569 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004570 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004571 help='Set the review private. This implies --no-autocc.')
4572
rmistry@google.com2dd99862015-06-22 12:22:18 +00004573 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004574 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004575 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004576 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004577 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004578 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004579
sbc@chromium.org71437c02015-04-09 19:29:40 +00004580 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004581 return 1
4582
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004583 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004584 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004585 options.cc = cleanup_list(options.cc)
4586
tandriib80458a2016-06-23 12:20:07 -07004587 if options.message_file:
4588 if options.message:
4589 parser.error('only one of --message and --message-file allowed.')
4590 options.message = gclient_utils.FileRead(options.message_file)
4591 options.message_file = None
4592
tandrii4d0545a2016-07-06 03:56:49 -07004593 if options.cq_dry_run and options.use_commit_queue:
4594 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4595
Aaron Gableedbc4132017-09-11 13:22:28 -07004596 if options.use_commit_queue:
4597 options.send_mail = True
4598
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004599 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4600 settings.GetIsGerrit()
4601
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004602 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004603 if not cl.IsGerrit():
4604 # Error out with instructions for repos not yet configured for Gerrit.
4605 print('=====================================')
4606 print('NOTICE: Rietveld is no longer supported. '
4607 'You can upload changes to Gerrit with')
4608 print(' git cl upload --gerrit')
4609 print('or set Gerrit to be your default code review tool with')
4610 print(' git config gerrit.host true')
4611 print('=====================================')
4612 return 1
4613
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004614 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004615
4616
Francois Dorayd42c6812017-05-30 15:10:20 -04004617@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004618@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004619def CMDsplit(parser, args):
4620 """Splits a branch into smaller branches and uploads CLs.
4621
4622 Creates a branch and uploads a CL for each group of files modified in the
4623 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004624 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004625 the shared OWNERS file.
4626 """
4627 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004628 help="A text file containing a CL description in which "
4629 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004630 parser.add_option("-c", "--comment", dest="comment_file",
4631 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004632 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4633 default=False,
4634 help="List the files and reviewers for each CL that would "
4635 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004636 parser.add_option("--cq-dry-run", action='store_true',
4637 help="If set, will do a cq dry run for each uploaded CL. "
4638 "Please be careful when doing this; more than ~10 CLs "
4639 "has the potential to overload our build "
4640 "infrastructure. Try to upload these not during high "
4641 "load times (usually 11-3 Mountain View time). Email "
4642 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004643 options, _ = parser.parse_args(args)
4644
4645 if not options.description_file:
4646 parser.error('No --description flag specified.')
4647
4648 def WrappedCMDupload(args):
4649 return CMDupload(OptionParser(), args)
4650
4651 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004652 Changelist, WrappedCMDupload, options.dry_run,
4653 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04004654
4655
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004656@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004657@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004658def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004659 """DEPRECATED: Used to commit the current changelist via git-svn."""
4660 message = ('git-cl no longer supports committing to SVN repositories via '
4661 'git-svn. You probably want to use `git cl land` instead.')
4662 print(message)
4663 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004664
4665
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004666# Two special branches used by git cl land.
4667MERGE_BRANCH = 'git-cl-commit'
4668CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4669
4670
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004671@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004672@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004673def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004674 """Commits the current changelist via git.
4675
4676 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4677 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004678 """
4679 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4680 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004681 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004682 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004683 parser.add_option('--parallel', action='store_true',
4684 help='Run all tests specified by input_api.RunTests in all '
4685 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004686 auth.add_auth_options(parser)
4687 (options, args) = parser.parse_args(args)
4688 auth_config = auth.extract_auth_config_from_options(options)
4689
4690 cl = Changelist(auth_config=auth_config)
4691
Robert Iannucci2e73d432018-03-14 01:10:47 -07004692 if not cl.IsGerrit():
4693 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004694
Robert Iannucci2e73d432018-03-14 01:10:47 -07004695 if not cl.GetIssue():
4696 DieWithError('You must upload the change first to Gerrit.\n'
4697 ' If you would rather have `git cl land` upload '
4698 'automatically for you, see http://crbug.com/642759')
4699 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004700 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004701
4702
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004703@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004704@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004705def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004706 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004707 parser.add_option('-b', dest='newbranch',
4708 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004709 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004710 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004711 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004712 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004713 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004714 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004715 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004716 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004717 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004718 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004719
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004720
4721 group = optparse.OptionGroup(
4722 parser,
4723 'Options for continuing work on the current issue uploaded from a '
4724 'different clone (e.g. different machine). Must be used independently '
4725 'from the other options. No issue number should be specified, and the '
4726 'branch must have an issue number associated with it')
4727 group.add_option('--reapply', action='store_true', dest='reapply',
4728 help='Reset the branch and reapply the issue.\n'
4729 'CAUTION: This will undo any local changes in this '
4730 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004731
4732 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004733 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004734 parser.add_option_group(group)
4735
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004736 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004737 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004738 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004739 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004740 auth_config = auth.extract_auth_config_from_options(options)
4741
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004742 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004743 if options.newbranch:
4744 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004745 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004746 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004747
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004748 cl = Changelist(auth_config=auth_config,
4749 codereview=options.forced_codereview)
4750 if not cl.GetIssue():
4751 parser.error('current branch must have an associated issue')
4752
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004753 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004754 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004755 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004756
4757 RunGit(['reset', '--hard', upstream])
4758 if options.pull:
4759 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004760
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004761 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4762 options.directory)
4763
4764 if len(args) != 1 or not args[0]:
4765 parser.error('Must specify issue number or url')
4766
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004767 target_issue_arg = ParseIssueNumberArgument(args[0],
4768 options.forced_codereview)
4769 if not target_issue_arg.valid:
4770 parser.error('invalid codereview url or CL id')
4771
4772 cl_kwargs = {
4773 'auth_config': auth_config,
4774 'codereview_host': target_issue_arg.hostname,
4775 'codereview': options.forced_codereview,
4776 }
4777 detected_codereview_from_url = False
4778 if target_issue_arg.codereview and not options.forced_codereview:
4779 detected_codereview_from_url = True
4780 cl_kwargs['codereview'] = target_issue_arg.codereview
4781 cl_kwargs['issue'] = target_issue_arg.issue
4782
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004783 # We don't want uncommitted changes mixed up with the patch.
4784 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004785 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004786
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004787 if options.newbranch:
4788 if options.force:
4789 RunGit(['branch', '-D', options.newbranch],
4790 stderr=subprocess2.PIPE, error_ok=True)
4791 RunGit(['new-branch', options.newbranch])
4792
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004793 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004794
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004795 if cl.IsGerrit():
4796 if options.reject:
4797 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004798 if options.directory:
4799 parser.error('--directory is not supported with Gerrit codereview.')
4800
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004801 if detected_codereview_from_url:
4802 print('canonical issue/change URL: %s (type: %s)\n' %
4803 (cl.GetIssueURL(), target_issue_arg.codereview))
4804
4805 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07004806 options.nocommit, options.directory,
4807 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004808
4809
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004810def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004811 """Fetches the tree status and returns either 'open', 'closed',
4812 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004813 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004814 if url:
4815 status = urllib2.urlopen(url).read().lower()
4816 if status.find('closed') != -1 or status == '0':
4817 return 'closed'
4818 elif status.find('open') != -1 or status == '1':
4819 return 'open'
4820 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004821 return 'unset'
4822
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004824def GetTreeStatusReason():
4825 """Fetches the tree status from a json url and returns the message
4826 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004827 url = settings.GetTreeStatusUrl()
4828 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004829 connection = urllib2.urlopen(json_url)
4830 status = json.loads(connection.read())
4831 connection.close()
4832 return status['message']
4833
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004834
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004835@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004836def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004837 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004838 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004839 status = GetTreeStatus()
4840 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004841 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004842 return 2
4843
vapiera7fbd5a2016-06-16 09:17:49 -07004844 print('The tree is %s' % status)
4845 print()
4846 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004847 if status != 'open':
4848 return 1
4849 return 0
4850
4851
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004852@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004853def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004854 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004855 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004856 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004857 '-b', '--bot', action='append',
4858 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4859 'times to specify multiple builders. ex: '
4860 '"-b win_rel -b win_layout". See '
4861 'the try server waterfall for the builders name and the tests '
4862 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004863 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004864 '-B', '--bucket', default='',
4865 help=('Buildbucket bucket to send the try requests.'))
4866 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004867 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004868 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004869 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004870 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004871 help='Revision to use for the try job; default: the revision will '
4872 'be determined by the try recipe that builder runs, which usually '
4873 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004874 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004875 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004876 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004877 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004878 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004879 '--category', default='git_cl_try', help='Specify custom build category.')
4880 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004881 '--project',
4882 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004883 'in recipe to determine to which repository or directory to '
4884 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004885 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004886 '-p', '--property', dest='properties', action='append', default=[],
4887 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004888 'key2=value2 etc. The value will be treated as '
4889 'json if decodable, or as string otherwise. '
4890 'NOTE: using this may make your try job not usable for CQ, '
4891 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004892 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004893 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4894 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004895 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004896 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09004897 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004898 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09004899 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004900 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004901
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004902 if options.master and options.master.startswith('luci.'):
4903 parser.error(
4904 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00004905 # Make sure that all properties are prop=value pairs.
4906 bad_params = [x for x in options.properties if '=' not in x]
4907 if bad_params:
4908 parser.error('Got properties with missing "=": %s' % bad_params)
4909
maruel@chromium.org15192402012-09-06 12:38:29 +00004910 if args:
4911 parser.error('Unknown arguments: %s' % args)
4912
Koji Ishii31c14782018-01-08 17:17:33 +09004913 cl = Changelist(auth_config=auth_config, issue=options.issue,
4914 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00004915 if not cl.GetIssue():
4916 parser.error('Need to upload first')
4917
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004918 if cl.IsGerrit():
4919 # HACK: warm up Gerrit change detail cache to save on RPCs.
4920 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4921
tandriie113dfd2016-10-11 10:20:12 -07004922 error_message = cl.CannotTriggerTryJobReason()
4923 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004924 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004925
borenet6c0efe62016-10-19 08:13:29 -07004926 if options.bucket and options.master:
4927 parser.error('Only one of --bucket and --master may be used.')
4928
qyearsley1fdfcb62016-10-24 13:22:03 -07004929 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004930
qyearsleydd49f942016-10-28 11:57:22 -07004931 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4932 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004933 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004934 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004935 print('git cl try with no bots now defaults to CQ dry run.')
4936 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4937 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004938
borenet6c0efe62016-10-19 08:13:29 -07004939 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004940 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004941 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004942 'of bot requires an initial job from a parent (usually a builder). '
4943 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004944 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004945 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004946
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004947 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07004948 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004949 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07004950 except BuildbucketResponseException as ex:
4951 print('ERROR: %s' % ex)
4952 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004953 return 0
4954
4955
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004956@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004957def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004958 """Prints info about try jobs associated with current CL."""
4959 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004960 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004961 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004962 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004963 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004964 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004965 '--color', action='store_true', default=setup_color.IS_TTY,
4966 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004967 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004968 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4969 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004970 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07004971 '--json', help=('Path of JSON output file to write try job results to,'
4972 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004973 parser.add_option_group(group)
4974 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07004975 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004976 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07004977 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004978 if args:
4979 parser.error('Unrecognized args: %s' % ' '.join(args))
4980
4981 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07004982 cl = Changelist(
4983 issue=options.issue, codereview=options.forced_codereview,
4984 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004985 if not cl.GetIssue():
4986 parser.error('Need to upload first')
4987
tandrii221ab252016-10-06 08:12:04 -07004988 patchset = options.patchset
4989 if not patchset:
4990 patchset = cl.GetMostRecentPatchset()
4991 if not patchset:
4992 parser.error('Codereview doesn\'t know about issue %s. '
4993 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004994 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07004995 cl.GetIssue())
4996
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004997 try:
tandrii221ab252016-10-06 08:12:04 -07004998 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004999 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005000 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005001 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005002 if options.json:
5003 write_try_results_json(options.json, jobs)
5004 else:
5005 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005006 return 0
5007
5008
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005009@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005010@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005011def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005012 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005013 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005014 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005015 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005016
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005017 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005018 if args:
5019 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005020 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005021 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005022 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005023 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005024
5025 # Clear configured merge-base, if there is one.
5026 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005027 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005028 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005029 return 0
5030
5031
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005032@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005033def CMDweb(parser, args):
5034 """Opens the current CL in the web browser."""
5035 _, args = parser.parse_args(args)
5036 if args:
5037 parser.error('Unrecognized args: %s' % ' '.join(args))
5038
5039 issue_url = Changelist().GetIssueURL()
5040 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005041 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005042 return 1
5043
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005044 # Redirect I/O before invoking browser to hide its output. For example, this
5045 # allows to hide "Created new window in existing browser session." message
5046 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5047 saved_stdout = os.dup(1)
5048 os.close(1)
5049 os.open(os.devnull, os.O_RDWR)
5050 try:
5051 webbrowser.open(issue_url)
5052 finally:
5053 os.dup2(saved_stdout, 1)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005054 return 0
5055
5056
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005057@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005058def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005059 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005060 parser.add_option('-d', '--dry-run', action='store_true',
5061 help='trigger in dry run mode')
5062 parser.add_option('-c', '--clear', action='store_true',
5063 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005064 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005065 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005066 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005067 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005068 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005069 if args:
5070 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005071 if options.dry_run and options.clear:
5072 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5073
iannuccie53c9352016-08-17 14:40:40 -07005074 cl = Changelist(auth_config=auth_config, issue=options.issue,
5075 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005076 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005077 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005078 elif options.dry_run:
5079 state = _CQState.DRY_RUN
5080 else:
5081 state = _CQState.COMMIT
5082 if not cl.GetIssue():
5083 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005084 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005085 return 0
5086
5087
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005088@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005089def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005090 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005091 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005092 auth.add_auth_options(parser)
5093 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005094 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005095 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005096 if args:
5097 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005098 cl = Changelist(auth_config=auth_config, issue=options.issue,
5099 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005100 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005101 if not cl.GetIssue():
5102 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005103 cl.CloseIssue()
5104 return 0
5105
5106
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005107@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005108def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005109 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005110 parser.add_option(
5111 '--stat',
5112 action='store_true',
5113 dest='stat',
5114 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005115 auth.add_auth_options(parser)
5116 options, args = parser.parse_args(args)
5117 auth_config = auth.extract_auth_config_from_options(options)
5118 if args:
5119 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005120
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005121 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005122 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005123 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005124 if not issue:
5125 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005126
Aaron Gablea718c3e2017-08-28 17:47:28 -07005127 base = cl._GitGetBranchConfigValue('last-upload-hash')
5128 if not base:
5129 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5130 if not base:
5131 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5132 revision_info = detail['revisions'][detail['current_revision']]
5133 fetch_info = revision_info['fetch']['http']
5134 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5135 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005136
Aaron Gablea718c3e2017-08-28 17:47:28 -07005137 cmd = ['git', 'diff']
5138 if options.stat:
5139 cmd.append('--stat')
5140 cmd.append(base)
5141 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005142
5143 return 0
5144
5145
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005146@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005147def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005148 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005149 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005150 '--ignore-current',
5151 action='store_true',
5152 help='Ignore the CL\'s current reviewers and start from scratch.')
5153 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005154 '--no-color',
5155 action='store_true',
5156 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005157 parser.add_option(
5158 '--batch',
5159 action='store_true',
5160 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005161 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005162 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005163 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005164
5165 author = RunGit(['config', 'user.email']).strip() or None
5166
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005167 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005168
5169 if args:
5170 if len(args) > 1:
5171 parser.error('Unknown args')
5172 base_branch = args[0]
5173 else:
5174 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005175 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005176
5177 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005178 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5179
5180 if options.batch:
5181 db = owners.Database(change.RepositoryRoot(), file, os.path)
5182 print('\n'.join(db.reviewers_for(affected_files, author)))
5183 return 0
5184
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005185 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005186 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005187 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005188 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005189 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005190 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005191 disable_color=options.no_color,
5192 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005193
5194
Aiden Bennerc08566e2018-10-03 17:52:42 +00005195def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005196 """Generates a diff command."""
5197 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005198 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5199
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005200 if allow_prefix:
5201 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5202 # case that diff.noprefix is set in the user's git config.
5203 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5204 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005205 diff_cmd += ['--no-prefix']
5206
5207 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005208
5209 if args:
5210 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005211 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005212 diff_cmd.append(arg)
5213 else:
5214 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005215
5216 return diff_cmd
5217
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005218
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005219def MatchingFileType(file_name, extensions):
5220 """Returns true if the file name ends with one of the given extensions."""
5221 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005222
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005223
enne@chromium.org555cfe42014-01-29 18:21:39 +00005224@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005225@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005226def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005227 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005228 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005229 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005230 parser.add_option('--full', action='store_true',
5231 help='Reformat the full content of all touched files')
5232 parser.add_option('--dry-run', action='store_true',
5233 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005234 parser.add_option(
5235 '--python',
5236 action='store_true',
5237 default=None,
5238 help='Enables python formatting on all python files.')
5239 parser.add_option(
5240 '--no-python',
5241 action='store_true',
5242 dest='python',
5243 help='Disables python formatting on all python files. '
5244 'Takes precedence over --python. '
5245 'If neither --python or --no-python are set, python '
5246 'files that have a .style.yapf file in an ancestor '
5247 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005248 parser.add_option('--js', action='store_true',
5249 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005250 parser.add_option('--diff', action='store_true',
5251 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005252 parser.add_option('--presubmit', action='store_true',
5253 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005254 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005255
Daniel Chengc55eecf2016-12-30 03:11:02 -08005256 # Normalize any remaining args against the current path, so paths relative to
5257 # the current directory are still resolved as expected.
5258 args = [os.path.join(os.getcwd(), arg) for arg in args]
5259
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005260 # git diff generates paths against the root of the repository. Change
5261 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005262 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005263 if rel_base_path:
5264 os.chdir(rel_base_path)
5265
digit@chromium.org29e47272013-05-17 17:01:46 +00005266 # Grab the merge-base commit, i.e. the upstream commit of the current
5267 # branch when it was created or the last time it was rebased. This is
5268 # to cover the case where the user may have called "git fetch origin",
5269 # moving the origin branch to a newer commit, but hasn't rebased yet.
5270 upstream_commit = None
5271 cl = Changelist()
5272 upstream_branch = cl.GetUpstreamBranch()
5273 if upstream_branch:
5274 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5275 upstream_commit = upstream_commit.strip()
5276
5277 if not upstream_commit:
5278 DieWithError('Could not find base commit for this branch. '
5279 'Are you in detached state?')
5280
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005281 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5282 diff_output = RunGit(changed_files_cmd)
5283 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005284 # Filter out files deleted by this CL
5285 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005286
Christopher Lamc5ba6922017-01-24 11:19:14 +11005287 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005288 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005289
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005290 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5291 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5292 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005293 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005294
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005295 top_dir = os.path.normpath(
5296 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5297
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005298 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5299 # formatted. This is used to block during the presubmit.
5300 return_value = 0
5301
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005302 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005303 # Locate the clang-format binary in the checkout
5304 try:
5305 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005306 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005307 DieWithError(e)
5308
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005309 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005310 cmd = [clang_format_tool]
5311 if not opts.dry_run and not opts.diff:
5312 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005313 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005314 if opts.diff:
5315 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005316 else:
5317 env = os.environ.copy()
5318 env['PATH'] = str(os.path.dirname(clang_format_tool))
5319 try:
5320 script = clang_format.FindClangFormatScriptInChromiumTree(
5321 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005322 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005323 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005324
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005325 cmd = [sys.executable, script, '-p0']
5326 if not opts.dry_run and not opts.diff:
5327 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005328
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005329 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5330 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005331
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005332 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5333 if opts.diff:
5334 sys.stdout.write(stdout)
5335 if opts.dry_run and len(stdout) > 0:
5336 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005337
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005338 # Similar code to above, but using yapf on .py files rather than clang-format
5339 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005340 py_explicitly_disabled = opts.python is not None and not opts.python
5341 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005342 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5343 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5344 if sys.platform.startswith('win'):
5345 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005346
Aiden Bennerc08566e2018-10-03 17:52:42 +00005347 # If we couldn't find a yapf file we'll default to the chromium style
5348 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005349 chromium_default_yapf_style = os.path.join(depot_tools_path,
5350 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005351 # Used for caching.
5352 yapf_configs = {}
5353 for f in python_diff_files:
5354 # Find the yapf style config for the current file, defaults to depot
5355 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005356 _FindYapfConfigFile(f, yapf_configs, top_dir)
5357
5358 # Turn on python formatting by default if a yapf config is specified.
5359 # This breaks in the case of this repo though since the specified
5360 # style file is also the global default.
5361 if opts.python is None:
5362 filtered_py_files = []
5363 for f in python_diff_files:
5364 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5365 filtered_py_files.append(f)
5366 else:
5367 filtered_py_files = python_diff_files
5368
5369 # Note: yapf still seems to fix indentation of the entire file
5370 # even if line ranges are specified.
5371 # See https://github.com/google/yapf/issues/499
5372 if not opts.full and filtered_py_files:
5373 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5374
5375 for f in filtered_py_files:
5376 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5377 if yapf_config is None:
5378 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005379
5380 cmd = [yapf_tool, '--style', yapf_config, f]
5381
5382 has_formattable_lines = False
5383 if not opts.full:
5384 # Only run yapf over changed line ranges.
5385 for diff_start, diff_len in py_line_diffs[f]:
5386 diff_end = diff_start + diff_len - 1
5387 # Yapf errors out if diff_end < diff_start but this
5388 # is a valid line range diff for a removal.
5389 if diff_end >= diff_start:
5390 has_formattable_lines = True
5391 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5392 # If all line diffs were removals we have nothing to format.
5393 if not has_formattable_lines:
5394 continue
5395
5396 if opts.diff or opts.dry_run:
5397 cmd += ['--diff']
5398 # Will return non-zero exit code if non-empty diff.
5399 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5400 if opts.diff:
5401 sys.stdout.write(stdout)
5402 elif len(stdout) > 0:
5403 return_value = 2
5404 else:
5405 cmd += ['-i']
5406 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005407
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005408 # Dart's formatter does not have the nice property of only operating on
5409 # modified chunks, so hard code full.
5410 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005411 try:
5412 command = [dart_format.FindDartFmtToolInChromiumTree()]
5413 if not opts.dry_run and not opts.diff:
5414 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005415 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005416
ppi@chromium.org6593d932016-03-03 15:41:15 +00005417 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005418 if opts.dry_run and stdout:
5419 return_value = 2
5420 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005421 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5422 'found in this checkout. Files in other languages are still '
5423 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005424
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005425 # Format GN build files. Always run on full build files for canonical form.
5426 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005427 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005428 if opts.dry_run or opts.diff:
5429 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005430 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005431 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5432 shell=sys.platform == 'win32',
5433 cwd=top_dir)
5434 if opts.dry_run and gn_ret == 2:
5435 return_value = 2 # Not formatted.
5436 elif opts.diff and gn_ret == 2:
5437 # TODO this should compute and print the actual diff.
5438 print("This change has GN build file diff for " + gn_diff_file)
5439 elif gn_ret != 0:
5440 # For non-dry run cases (and non-2 return values for dry-run), a
5441 # nonzero error code indicates a failure, probably because the file
5442 # doesn't parse.
5443 DieWithError("gn format failed on " + gn_diff_file +
5444 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005445
Ilya Shermane081cbe2017-08-15 17:51:04 -07005446 # Skip the metrics formatting from the global presubmit hook. These files have
5447 # a separate presubmit hook that issues an error if the files need formatting,
5448 # whereas the top-level presubmit script merely issues a warning. Formatting
5449 # these files is somewhat slow, so it's important not to duplicate the work.
5450 if not opts.presubmit:
5451 for xml_dir in GetDirtyMetricsDirs(diff_files):
5452 tool_dir = os.path.join(top_dir, xml_dir)
5453 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5454 if opts.dry_run or opts.diff:
5455 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005456 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005457 if opts.diff:
5458 sys.stdout.write(stdout)
5459 if opts.dry_run and stdout:
5460 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005461
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005462 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005463
Steven Holte2e664bf2017-04-21 13:10:47 -07005464def GetDirtyMetricsDirs(diff_files):
5465 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5466 metrics_xml_dirs = [
5467 os.path.join('tools', 'metrics', 'actions'),
5468 os.path.join('tools', 'metrics', 'histograms'),
5469 os.path.join('tools', 'metrics', 'rappor'),
5470 os.path.join('tools', 'metrics', 'ukm')]
5471 for xml_dir in metrics_xml_dirs:
5472 if any(file.startswith(xml_dir) for file in xml_diff_files):
5473 yield xml_dir
5474
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005475
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005476@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005477@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005478def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005479 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005480 _, args = parser.parse_args(args)
5481
5482 if len(args) != 1:
5483 parser.print_help()
5484 return 1
5485
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005486 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005487 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005488 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005489
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005490 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005491
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005492 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005493 output = RunGit(['config', '--local', '--get-regexp',
5494 r'branch\..*\.%s' % issueprefix],
5495 error_ok=True)
5496 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005497 if issue == target_issue:
5498 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005499
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005500 branches = []
5501 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005502 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005503 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005504 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005505 return 1
5506 if len(branches) == 1:
5507 RunGit(['checkout', branches[0]])
5508 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005509 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005510 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005511 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005512 which = raw_input('Choose by index: ')
5513 try:
5514 RunGit(['checkout', branches[int(which)]])
5515 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005516 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005517 return 1
5518
5519 return 0
5520
5521
maruel@chromium.org29404b52014-09-08 22:58:00 +00005522def CMDlol(parser, args):
5523 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005524 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005525 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5526 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5527 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005528 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005529 return 0
5530
5531
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005532class OptionParser(optparse.OptionParser):
5533 """Creates the option parse and add --verbose support."""
5534 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005535 optparse.OptionParser.__init__(
5536 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005537 self.add_option(
5538 '-v', '--verbose', action='count', default=0,
5539 help='Use 2 times for more debugging info')
5540
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005541 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005542 try:
5543 return self._parse_args(args)
5544 finally:
5545 # Regardless of success or failure of args parsing, we want to report
5546 # metrics, but only after logging has been initialized (if parsing
5547 # succeeded).
5548 global settings
5549 settings = Settings()
5550
5551 if not metrics.DISABLE_METRICS_COLLECTION:
5552 # GetViewVCUrl ultimately calls logging method.
5553 project_url = settings.GetViewVCUrl().strip('/+')
5554 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5555 metrics.collector.add('project_urls', [project_url])
5556
5557 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005558 # Create an optparse.Values object that will store only the actual passed
5559 # options, without the defaults.
5560 actual_options = optparse.Values()
5561 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5562 # Create an optparse.Values object with the default options.
5563 options = optparse.Values(self.get_default_values().__dict__)
5564 # Update it with the options passed by the user.
5565 options._update_careful(actual_options.__dict__)
5566 # Store the options passed by the user in an _actual_options attribute.
5567 # We store only the keys, and not the values, since the values can contain
5568 # arbitrary information, which might be PII.
5569 metrics.collector.add('arguments', actual_options.__dict__.keys())
5570
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005571 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005572 logging.basicConfig(
5573 level=levels[min(options.verbose, len(levels) - 1)],
5574 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5575 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005576
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005577 return options, args
5578
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005580def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005581 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005582 print('\nYour python version %s is unsupported, please upgrade.\n' %
5583 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005584 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005585
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005586 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005587 dispatcher = subcommand.CommandDispatcher(__name__)
5588 try:
5589 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005590 except auth.AuthenticationError as e:
5591 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005592 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005593 if e.code != 500:
5594 raise
5595 DieWithError(
5596 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5597 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005598 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005599
5600
5601if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005602 # These affect sys.stdout so do it outside of main() to simplify mocks in
5603 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005604 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005605 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005606 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005607 sys.exit(main(sys.argv[1:]))