blob: 1631be49b05a7c6823de724e5627b0c7c4d86211 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000032import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000033import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000035import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000036import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000037import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000038import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000039
40try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080041 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042except ImportError:
43 pass
44
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000045from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000046from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000058import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000059import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000060import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000061import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import presubmit_support
63import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040064import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067import watchlists
68
tandrii7400cf02016-06-21 08:48:07 -070069__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000070
tandrii9d2c7a32016-06-22 03:42:45 -070071COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080072POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000074REFS_THAT_ALIAS_TO_OTHER_REFS = {
75 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
76 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
77}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078
thestig@chromium.org44202a22014-03-11 19:22:18 +000079# Valid extensions for files we want to lint.
80DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
81DEFAULT_LINT_IGNORE_REGEX = r"$^"
82
Aiden Bennerc08566e2018-10-03 17:52:42 +000083# File name for yapf style config files.
84YAPF_CONFIG_FILENAME = '.style.yapf'
85
borenet6c0efe62016-10-19 08:13:29 -070086# Buildbucket master name prefix.
87MASTER_PREFIX = 'master.'
88
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000089# Shortcut since it quickly becomes redundant.
90Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000091
maruel@chromium.orgddd59412011-11-30 14:20:38 +000092# Initialized in main()
93settings = None
94
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010095# Used by tests/git_cl_test.py to add extra logging.
96# Inside the weirdly failing test, add this:
97# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -070098# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010099_IS_BEING_TESTED = False
100
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000101
Christopher Lamf732cd52017-01-24 12:40:11 +1100102def DieWithError(message, change_desc=None):
103 if change_desc:
104 SaveDescriptionBackup(change_desc)
105
vapiera7fbd5a2016-06-16 09:17:49 -0700106 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107 sys.exit(1)
108
109
Christopher Lamf732cd52017-01-24 12:40:11 +1100110def SaveDescriptionBackup(change_desc):
111 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000112 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100113 backup_file = open(backup_path, 'w')
114 backup_file.write(change_desc.description)
115 backup_file.close()
116
117
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000118def GetNoGitPagerEnv():
119 env = os.environ.copy()
120 # 'cat' is a magical git string that disables pagers on all platforms.
121 env['GIT_PAGER'] = 'cat'
122 return env
123
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000124
bsep@chromium.org627d9002016-04-29 00:00:52 +0000125def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000126 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000127 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000128 except subprocess2.CalledProcessError as e:
129 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000130 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000132 'Command "%s" failed.\n%s' % (
133 ' '.join(args), error_message or e.stdout or ''))
134 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000135
136
137def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000138 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000139 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000140
141
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000142def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000143 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700144 if suppress_stderr:
145 stderr = subprocess2.VOID
146 else:
147 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000148 try:
tandrii5d48c322016-08-18 16:19:37 -0700149 (out, _), code = subprocess2.communicate(['git'] + args,
150 env=GetNoGitPagerEnv(),
151 stdout=subprocess2.PIPE,
152 stderr=stderr)
153 return code, out
154 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900155 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700156 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000157
158
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000159def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000160 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000161 return RunGitWithCode(args, suppress_stderr=True)[1]
162
163
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000164def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000165 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000166 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000167 return (version.startswith(prefix) and
168 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000169
170
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000171def BranchExists(branch):
172 """Return True if specified branch exists."""
173 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
174 suppress_stderr=True)
175 return not code
176
177
tandrii2a16b952016-10-19 07:09:44 -0700178def time_sleep(seconds):
179 # Use this so that it can be mocked in tests without interfering with python
180 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700181 return time.sleep(seconds)
182
183
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000184def time_time():
185 # Use this so that it can be mocked in tests without interfering with python
186 # system machinery.
187 return time.time()
188
189
maruel@chromium.org90541732011-04-01 17:54:18 +0000190def ask_for_data(prompt):
191 try:
192 return raw_input(prompt)
193 except KeyboardInterrupt:
194 # Hide the exception.
195 sys.exit(1)
196
197
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100198def confirm_or_exit(prefix='', action='confirm'):
199 """Asks user to press enter to continue or press Ctrl+C to abort."""
200 if not prefix or prefix.endswith('\n'):
201 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100202 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100203 mid = ' Press'
204 elif prefix.endswith(' '):
205 mid = 'press'
206 else:
207 mid = ' press'
208 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
209
210
211def ask_for_explicit_yes(prompt):
212 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
213 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
214 while True:
215 if 'yes'.startswith(result):
216 return True
217 if 'no'.startswith(result):
218 return False
219 result = ask_for_data('Please, type yes or no: ').lower()
220
221
tandrii5d48c322016-08-18 16:19:37 -0700222def _git_branch_config_key(branch, key):
223 """Helper method to return Git config key for a branch."""
224 assert branch, 'branch name is required to set git config for it'
225 return 'branch.%s.%s' % (branch, key)
226
227
228def _git_get_branch_config_value(key, default=None, value_type=str,
229 branch=False):
230 """Returns git config value of given or current branch if any.
231
232 Returns default in all other cases.
233 """
234 assert value_type in (int, str, bool)
235 if branch is False: # Distinguishing default arg value from None.
236 branch = GetCurrentBranch()
237
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000238 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700239 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000240
tandrii5d48c322016-08-18 16:19:37 -0700241 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700242 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700243 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700244 # git config also has --int, but apparently git config suffers from integer
245 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700246 args.append(_git_branch_config_key(branch, key))
247 code, out = RunGitWithCode(args)
248 if code == 0:
249 value = out.strip()
250 if value_type == int:
251 return int(value)
252 if value_type == bool:
253 return bool(value.lower() == 'true')
254 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255 return default
256
257
tandrii5d48c322016-08-18 16:19:37 -0700258def _git_set_branch_config_value(key, value, branch=None, **kwargs):
259 """Sets the value or unsets if it's None of a git branch config.
260
261 Valid, though not necessarily existing, branch must be provided,
262 otherwise currently checked out branch is used.
263 """
264 if not branch:
265 branch = GetCurrentBranch()
266 assert branch, 'a branch name OR currently checked out branch is required'
267 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700268 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700269 if value is None:
270 args.append('--unset')
271 elif isinstance(value, bool):
272 args.append('--bool')
273 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700274 else:
tandrii33a46ff2016-08-23 05:53:40 -0700275 # git config also has --int, but apparently git config suffers from integer
276 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700277 value = str(value)
278 args.append(_git_branch_config_key(branch, key))
279 if value is not None:
280 args.append(value)
281 RunGit(args, **kwargs)
282
283
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100284def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700285 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100286
287 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
288 """
289 # Git also stores timezone offset, but it only affects visual display,
290 # actual point in time is defined by this timestamp only.
291 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
292
293
294def _git_amend_head(message, committer_timestamp):
295 """Amends commit with new message and desired committer_timestamp.
296
297 Sets committer timezone to UTC.
298 """
299 env = os.environ.copy()
300 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
301 return RunGit(['commit', '--amend', '-m', message], env=env)
302
303
machenbach@chromium.org45453142015-09-15 08:45:22 +0000304def _get_properties_from_options(options):
305 properties = dict(x.split('=', 1) for x in options.properties)
306 for key, val in properties.iteritems():
307 try:
308 properties[key] = json.loads(val)
309 except ValueError:
310 pass # If a value couldn't be evaluated, treat it as a string.
311 return properties
312
313
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000314def _prefix_master(master):
315 """Convert user-specified master name to full master name.
316
317 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
318 name, while the developers always use shortened master name
319 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
320 function does the conversion for buildbucket migration.
321 """
borenet6c0efe62016-10-19 08:13:29 -0700322 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000323 return master
borenet6c0efe62016-10-19 08:13:29 -0700324 return '%s%s' % (MASTER_PREFIX, master)
325
326
327def _unprefix_master(bucket):
328 """Convert bucket name to shortened master name.
329
330 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
331 name, while the developers always use shortened master name
332 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
333 function does the conversion for buildbucket migration.
334 """
335 if bucket.startswith(MASTER_PREFIX):
336 return bucket[len(MASTER_PREFIX):]
337 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338
339
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000340def _buildbucket_retry(operation_name, http, *args, **kwargs):
341 """Retries requests to buildbucket service and returns parsed json content."""
342 try_count = 0
343 while True:
344 response, content = http.request(*args, **kwargs)
345 try:
346 content_json = json.loads(content)
347 except ValueError:
348 content_json = None
349
350 # Buildbucket could return an error even if status==200.
351 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000352 error = content_json.get('error')
353 if error.get('code') == 403:
354 raise BuildbucketResponseException(
355 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000356 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000357 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 raise BuildbucketResponseException(msg)
359
360 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700361 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 raise BuildbucketResponseException(
363 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700364 'Please file bugs at http://crbug.com, '
365 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366 content)
367 return content_json
368 if response.status < 500 or try_count >= 2:
369 raise httplib2.HttpLib2Error(content)
370
371 # status >= 500 means transient failures.
372 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700373 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374 try_count += 1
375 assert False, 'unreachable'
376
377
qyearsley1fdfcb62016-10-24 13:22:03 -0700378def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700379 """Returns a dict mapping bucket names to builders and tests,
380 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700381 """
qyearsleydd49f942016-10-28 11:57:22 -0700382 # If no bots are listed, we try to get a set of builders and tests based
383 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700384 if not options.bot:
385 change = changelist.GetChange(
386 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700387 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700388 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700389 change=change,
390 changed_files=change.LocalPaths(),
391 repository_root=settings.GetRoot(),
392 default_presubmit=None,
393 project=None,
394 verbose=options.verbose,
395 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700396 if masters is None:
397 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100398 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700399
qyearsley1fdfcb62016-10-24 13:22:03 -0700400 if options.bucket:
401 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700402 if options.master:
403 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700404
qyearsleydd49f942016-10-28 11:57:22 -0700405 # If bots are listed but no master or bucket, then we need to find out
406 # the corresponding master for each bot.
407 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
408 if error_message:
409 option_parser.error(
410 'Tryserver master cannot be found because: %s\n'
411 'Please manually specify the tryserver master, e.g. '
412 '"-m tryserver.chromium.linux".' % error_message)
413 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700414
415
qyearsley123a4682016-10-26 09:12:17 -0700416def _get_bucket_map_for_builders(builders):
417 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700418 map_url = 'https://builders-map.appspot.com/'
419 try:
qyearsley123a4682016-10-26 09:12:17 -0700420 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700421 except urllib2.URLError as e:
422 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
423 (map_url, e))
424 except ValueError as e:
425 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700426 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700427 return None, 'Failed to build master map.'
428
qyearsley123a4682016-10-26 09:12:17 -0700429 bucket_map = {}
430 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800431 bucket = builders_map.get(builder, {}).get('bucket')
432 if bucket:
433 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700434 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700435
436
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800437def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700438 """Sends a request to Buildbucket to trigger try jobs for a changelist.
439
440 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700441 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700442 changelist: Changelist that the try jobs are associated with.
443 buckets: A nested dict mapping bucket names to builders to tests.
444 options: Command-line options.
445 """
tandriide281ae2016-10-12 06:02:30 -0700446 assert changelist.GetIssue(), 'CL must be uploaded first'
447 codereview_url = changelist.GetCodereviewServer()
448 assert codereview_url, 'CL must be uploaded first'
449 patchset = patchset or changelist.GetMostRecentPatchset()
450 assert patchset, 'CL must be uploaded first'
451
452 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700453 # Cache the buildbucket credentials under the codereview host key, so that
454 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700455 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000456 http = authenticator.authorize(httplib2.Http())
457 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700458
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459 buildbucket_put_url = (
460 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000461 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000462 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700463 hostname=codereview_host,
464 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000465 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700466
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700467 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800468 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700469 if options.clobber:
470 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700471 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700472 if extra_properties:
473 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000474
475 batch_req_body = {'builds': []}
476 print_text = []
477 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700478 for bucket, builders_and_tests in sorted(buckets.iteritems()):
479 print_text.append('Bucket: %s' % bucket)
480 master = None
481 if bucket.startswith(MASTER_PREFIX):
482 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000483 for builder, tests in sorted(builders_and_tests.iteritems()):
484 print_text.append(' %s: %s' % (builder, tests))
485 parameters = {
486 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000487 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100488 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000489 'revision': options.revision,
490 }],
tandrii8c5a3532016-11-04 07:52:02 -0700491 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000492 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000493 if 'presubmit' in builder.lower():
494 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000495 if tests:
496 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700497
498 tags = [
499 'builder:%s' % builder,
500 'buildset:%s' % buildset,
501 'user_agent:git_cl_try',
502 ]
503 if master:
504 parameters['properties']['master'] = master
505 tags.append('master:%s' % master)
506
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000507 batch_req_body['builds'].append(
508 {
509 'bucket': bucket,
510 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700512 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000513 }
514 )
515
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000516 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700517 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000518 http,
519 buildbucket_put_url,
520 'PUT',
521 body=json.dumps(batch_req_body),
522 headers={'Content-Type': 'application/json'}
523 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000524 print_text.append('To see results here, run: git cl try-results')
525 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700526 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000527
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000528
tandrii221ab252016-10-06 08:12:04 -0700529def fetch_try_jobs(auth_config, changelist, buildbucket_host,
530 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700531 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000532
qyearsley53f48a12016-09-01 10:45:13 -0700533 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534 """
tandrii221ab252016-10-06 08:12:04 -0700535 assert buildbucket_host
536 assert changelist.GetIssue(), 'CL must be uploaded first'
537 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
538 patchset = patchset or changelist.GetMostRecentPatchset()
539 assert patchset, 'CL must be uploaded first'
540
541 codereview_url = changelist.GetCodereviewServer()
542 codereview_host = urlparse.urlparse(codereview_url).hostname
543 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 if authenticator.has_cached_credentials():
545 http = authenticator.authorize(httplib2.Http())
546 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700547 print('Warning: Some results might be missing because %s' %
548 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700549 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 http = httplib2.Http()
551
552 http.force_exception_to_status_code = True
553
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000554 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700555 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700557 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 params = {'tag': 'buildset:%s' % buildset}
559
560 builds = {}
561 while True:
562 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700563 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700565 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566 for build in content.get('builds', []):
567 builds[build['id']] = build
568 if 'next_cursor' in content:
569 params['start_cursor'] = content['next_cursor']
570 else:
571 break
572 return builds
573
574
qyearsleyeab3c042016-08-24 09:18:28 -0700575def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576 """Prints nicely result of fetch_try_jobs."""
577 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700578 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 return
580
581 # Make a copy, because we'll be modifying builds dictionary.
582 builds = builds.copy()
583 builder_names_cache = {}
584
585 def get_builder(b):
586 try:
587 return builder_names_cache[b['id']]
588 except KeyError:
589 try:
590 parameters = json.loads(b['parameters_json'])
591 name = parameters['builder_name']
592 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700593 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700594 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000595 name = None
596 builder_names_cache[b['id']] = name
597 return name
598
599 def get_bucket(b):
600 bucket = b['bucket']
601 if bucket.startswith('master.'):
602 return bucket[len('master.'):]
603 return bucket
604
605 if options.print_master:
606 name_fmt = '%%-%ds %%-%ds' % (
607 max(len(str(get_bucket(b))) for b in builds.itervalues()),
608 max(len(str(get_builder(b))) for b in builds.itervalues()))
609 def get_name(b):
610 return name_fmt % (get_bucket(b), get_builder(b))
611 else:
612 name_fmt = '%%-%ds' % (
613 max(len(str(get_builder(b))) for b in builds.itervalues()))
614 def get_name(b):
615 return name_fmt % get_builder(b)
616
617 def sort_key(b):
618 return b['status'], b.get('result'), get_name(b), b.get('url')
619
620 def pop(title, f, color=None, **kwargs):
621 """Pop matching builds from `builds` dict and print them."""
622
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000623 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000624 colorize = str
625 else:
626 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
627
628 result = []
629 for b in builds.values():
630 if all(b.get(k) == v for k, v in kwargs.iteritems()):
631 builds.pop(b['id'])
632 result.append(b)
633 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700634 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000635 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700636 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000637
638 total = len(builds)
639 pop(status='COMPLETED', result='SUCCESS',
640 title='Successes:', color=Fore.GREEN,
641 f=lambda b: (get_name(b), b.get('url')))
642 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
643 title='Infra Failures:', color=Fore.MAGENTA,
644 f=lambda b: (get_name(b), b.get('url')))
645 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
646 title='Failures:', color=Fore.RED,
647 f=lambda b: (get_name(b), b.get('url')))
648 pop(status='COMPLETED', result='CANCELED',
649 title='Canceled:', color=Fore.MAGENTA,
650 f=lambda b: (get_name(b),))
651 pop(status='COMPLETED', result='FAILURE',
652 failure_reason='INVALID_BUILD_DEFINITION',
653 title='Wrong master/builder name:', color=Fore.MAGENTA,
654 f=lambda b: (get_name(b),))
655 pop(status='COMPLETED', result='FAILURE',
656 title='Other failures:',
657 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
658 pop(status='COMPLETED',
659 title='Other finished:',
660 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
661 pop(status='STARTED',
662 title='Started:', color=Fore.YELLOW,
663 f=lambda b: (get_name(b), b.get('url')))
664 pop(status='SCHEDULED',
665 title='Scheduled:',
666 f=lambda b: (get_name(b), 'id=%s' % b['id']))
667 # The last section is just in case buildbucket API changes OR there is a bug.
668 pop(title='Other:',
669 f=lambda b: (get_name(b), 'id=%s' % b['id']))
670 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700671 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000672
673
Aiden Bennerc08566e2018-10-03 17:52:42 +0000674def _ComputeDiffLineRanges(files, upstream_commit):
675 """Gets the changed line ranges for each file since upstream_commit.
676
677 Parses a git diff on provided files and returns a dict that maps a file name
678 to an ordered list of range tuples in the form (start_line, count).
679 Ranges are in the same format as a git diff.
680 """
681 # If files is empty then diff_output will be a full diff.
682 if len(files) == 0:
683 return {}
684
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000685 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000686 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
687 diff_output = RunGit(diff_cmd)
688
689 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
690 # 2 capture groups
691 # 0 == fname of diff file
692 # 1 == 'diff_start,diff_count' or 'diff_start'
693 # will match each of
694 # diff --git a/foo.foo b/foo.py
695 # @@ -12,2 +14,3 @@
696 # @@ -12,2 +17 @@
697 # running re.findall on the above string with pattern will give
698 # [('foo.py', ''), ('', '14,3'), ('', '17')]
699
700 curr_file = None
701 line_diffs = {}
702 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
703 if match[0] != '':
704 # Will match the second filename in diff --git a/a.py b/b.py.
705 curr_file = match[0]
706 line_diffs[curr_file] = []
707 else:
708 # Matches +14,3
709 if ',' in match[1]:
710 diff_start, diff_count = match[1].split(',')
711 else:
712 # Single line changes are of the form +12 instead of +12,1.
713 diff_start = match[1]
714 diff_count = 1
715
716 diff_start = int(diff_start)
717 diff_count = int(diff_count)
718
719 # If diff_count == 0 this is a removal we can ignore.
720 line_diffs[curr_file].append((diff_start, diff_count))
721
722 return line_diffs
723
724
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000725def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000726 """Checks if a yapf file is in any parent directory of fpath until top_dir.
727
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000728 Recursively checks parent directories to find yapf file and if no yapf file
729 is found returns None. Uses yapf_config_cache as a cache for
730 previously found configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000731 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000732 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000733 # Return result if we've already computed it.
734 if fpath in yapf_config_cache:
735 return yapf_config_cache[fpath]
736
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000737 parent_dir = os.path.dirname(fpath)
738 if os.path.isfile(fpath):
739 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000740 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000741 # Otherwise fpath is a directory
742 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
743 if os.path.isfile(yapf_file):
744 ret = yapf_file
745 elif fpath == top_dir or parent_dir == fpath:
746 # If we're at the top level directory, or if we're at root
747 # there is no provided style.
748 ret = None
749 else:
750 # Otherwise recurse on the current directory.
751 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000752 yapf_config_cache[fpath] = ret
753 return ret
754
755
qyearsley53f48a12016-09-01 10:45:13 -0700756def write_try_results_json(output_file, builds):
757 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
758
759 The input |builds| dict is assumed to be generated by Buildbucket.
760 Buildbucket documentation: http://goo.gl/G0s101
761 """
762
763 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800764 """Extracts some of the information from one build dict."""
765 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700766 return {
767 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700768 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800769 'builder_name': parameters.get('builder_name'),
770 'created_ts': build.get('created_ts'),
771 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700772 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800773 'result': build.get('result'),
774 'status': build.get('status'),
775 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700776 'url': build.get('url'),
777 }
778
779 converted = []
780 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000781 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700782 write_json(output_file, converted)
783
784
Aaron Gable13101a62018-02-09 13:20:41 -0800785def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000786 """Prints statistics about the change to the user."""
787 # --no-ext-diff is broken in some versions of Git, so try to work around
788 # this by overriding the environment (but there is still a problem if the
789 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000790 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000791 if 'GIT_EXTERNAL_DIFF' in env:
792 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000793
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000794 try:
795 stdout = sys.stdout.fileno()
796 except AttributeError:
797 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000798 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800799 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000800 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000801
802
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000803class BuildbucketResponseException(Exception):
804 pass
805
806
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807class Settings(object):
808 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000810 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 self.tree_status_url = None
812 self.viewvc_url = None
813 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000814 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000815 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000816 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000817 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818
819 def LazyUpdateIfNeeded(self):
820 """Updates the settings from a codereview.settings file, if available."""
821 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000822 # The only value that actually changes the behavior is
823 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000824 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000825 error_ok=True
826 ).strip().lower()
827
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000829 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000830 LoadCodereviewSettingsFromFile(cr_settings_file)
831 self.updated = True
832
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000833 @staticmethod
834 def GetRelativeRoot():
835 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000838 if self.root is None:
839 self.root = os.path.abspath(self.GetRelativeRoot())
840 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 def GetTreeStatusUrl(self, error_ok=False):
843 if not self.tree_status_url:
844 error_message = ('You must configure your tree status URL by running '
845 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000846 self.tree_status_url = self._GetConfig(
847 'rietveld.tree-status-url', error_ok=error_ok,
848 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 return self.tree_status_url
850
851 def GetViewVCUrl(self):
852 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000853 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 return self.viewvc_url
855
rmistry@google.com90752582014-01-14 21:04:50 +0000856 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000857 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000858
rmistry@google.com5626a922015-02-26 14:03:30 +0000859 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000860 run_post_upload_hook = self._GetConfig(
861 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000862 return run_post_upload_hook == "True"
863
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000864 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000865 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000866
ukai@chromium.orge8077812012-02-03 03:41:46 +0000867 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700868 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000869 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700870 self.is_gerrit = (
871 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000872 return self.is_gerrit
873
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000874 def GetSquashGerritUploads(self):
875 """Return true if uploads to Gerrit should be squashed by default."""
876 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700877 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
878 if self.squash_gerrit_uploads is None:
879 # Default is squash now (http://crbug.com/611892#c23).
880 self.squash_gerrit_uploads = not (
881 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
882 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000883 return self.squash_gerrit_uploads
884
tandriia60502f2016-06-20 02:01:53 -0700885 def GetSquashGerritUploadsOverride(self):
886 """Return True or False if codereview.settings should be overridden.
887
888 Returns None if no override has been defined.
889 """
890 # See also http://crbug.com/611892#c23
891 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
892 error_ok=True).strip()
893 if result == 'true':
894 return True
895 if result == 'false':
896 return False
897 return None
898
tandrii@chromium.org28253532016-04-14 13:46:56 +0000899 def GetGerritSkipEnsureAuthenticated(self):
900 """Return True if EnsureAuthenticated should not be done for Gerrit
901 uploads."""
902 if self.gerrit_skip_ensure_authenticated is None:
903 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000904 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000905 error_ok=True).strip() == 'true')
906 return self.gerrit_skip_ensure_authenticated
907
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000908 def GetGitEditor(self):
909 """Return the editor specified in the git config, or None if none is."""
910 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000911 # Git requires single quotes for paths with spaces. We need to replace
912 # them with double quotes for Windows to treat such paths as a single
913 # path.
914 self.git_editor = self._GetConfig(
915 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000916 return self.git_editor or None
917
thestig@chromium.org44202a22014-03-11 19:22:18 +0000918 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000919 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000920 DEFAULT_LINT_REGEX)
921
922 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000923 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000924 DEFAULT_LINT_IGNORE_REGEX)
925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 def _GetConfig(self, param, **kwargs):
927 self.LazyUpdateIfNeeded()
928 return RunGit(['config', param], **kwargs).strip()
929
930
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100931@contextlib.contextmanager
932def _get_gerrit_project_config_file(remote_url):
933 """Context manager to fetch and store Gerrit's project.config from
934 refs/meta/config branch and store it in temp file.
935
936 Provides a temporary filename or None if there was error.
937 """
938 error, _ = RunGitWithCode([
939 'fetch', remote_url,
940 '+refs/meta/config:refs/git_cl/meta/config'])
941 if error:
942 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700943 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100944 (remote_url, error))
945 yield None
946 return
947
948 error, project_config_data = RunGitWithCode(
949 ['show', 'refs/git_cl/meta/config:project.config'])
950 if error:
951 print('WARNING: project.config file not found')
952 yield None
953 return
954
955 with gclient_utils.temporary_directory() as tempdir:
956 project_config_file = os.path.join(tempdir, 'project.config')
957 gclient_utils.FileWrite(project_config_file, project_config_data)
958 yield project_config_file
959
960
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961def ShortBranchName(branch):
962 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000963 return branch.replace('refs/heads/', '', 1)
964
965
966def GetCurrentBranchRef():
967 """Returns branch ref (e.g., refs/heads/master) or None."""
968 return RunGit(['symbolic-ref', 'HEAD'],
969 stderr=subprocess2.VOID, error_ok=True).strip() or None
970
971
972def GetCurrentBranch():
973 """Returns current branch or None.
974
975 For refs/heads/* branches, returns just last part. For others, full ref.
976 """
977 branchref = GetCurrentBranchRef()
978 if branchref:
979 return ShortBranchName(branchref)
980 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981
982
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000983class _CQState(object):
984 """Enum for states of CL with respect to Commit Queue."""
985 NONE = 'none'
986 DRY_RUN = 'dry_run'
987 COMMIT = 'commit'
988
989 ALL_STATES = [NONE, DRY_RUN, COMMIT]
990
991
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000992class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200993 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000994 self.issue = issue
995 self.patchset = patchset
996 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +0000997 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200998 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000999
1000 @property
1001 def valid(self):
1002 return self.issue is not None
1003
1004
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001005def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001006 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1007 fail_result = _ParsedIssueNumberArgument()
1008
1009 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001010 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001011 if not arg.startswith('http'):
1012 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001013
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001014 url = gclient_utils.UpgradeToHttps(arg)
1015 try:
1016 parsed_url = urlparse.urlparse(url)
1017 except ValueError:
1018 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001019
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001020 if codereview is not None:
1021 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1022 return parsed or fail_result
1023
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001024 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001025
1026
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001027def _create_description_from_log(args):
1028 """Pulls out the commit log to use as a base for the CL description."""
1029 log_args = []
1030 if len(args) == 1 and not args[0].endswith('.'):
1031 log_args = [args[0] + '..']
1032 elif len(args) == 1 and args[0].endswith('...'):
1033 log_args = [args[0][:-1]]
1034 elif len(args) == 2:
1035 log_args = [args[0] + '..' + args[1]]
1036 else:
1037 log_args = args[:] # Hope for the best!
1038 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1039
1040
Aaron Gablea45ee112016-11-22 15:14:38 -08001041class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001042 def __init__(self, issue, url):
1043 self.issue = issue
1044 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001045 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001046
1047 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001048 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001049 self.issue, self.url)
1050
1051
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001052_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001053 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001054 # TODO(tandrii): these two aren't known in Gerrit.
1055 'approval', 'disapproval'])
1056
1057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001059 """Changelist works with one changelist in local branch.
1060
1061 Supports two codereview backends: Rietveld or Gerrit, selected at object
1062 creation.
1063
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001064 Notes:
1065 * Not safe for concurrent multi-{thread,process} use.
1066 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001067 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001068 """
1069
1070 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1071 """Create a new ChangeList instance.
1072
1073 If issue is given, the codereview must be given too.
1074
1075 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1076 Otherwise, it's decided based on current configuration of the local branch,
1077 with default being 'rietveld' for backwards compatibility.
1078 See _load_codereview_impl for more details.
1079
1080 **kwargs will be passed directly to codereview implementation.
1081 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001082 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001083 global settings
1084 if not settings:
1085 # Happens when git_cl.py is used as a utility library.
1086 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001087
1088 if issue:
1089 assert codereview, 'codereview must be known, if issue is known'
1090
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091 self.branchref = branchref
1092 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001093 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094 self.branch = ShortBranchName(self.branchref)
1095 else:
1096 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001098 self.lookedup_issue = False
1099 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 self.has_description = False
1101 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001102 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001104 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001105 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001106 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001107 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001108
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001109 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001110 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001111 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001112 assert self._codereview_impl
1113 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001114
1115 def _load_codereview_impl(self, codereview=None, **kwargs):
1116 if codereview:
Joe Masond87b0962018-12-03 21:04:46 +00001117 assert codereview in _CODEREVIEW_IMPLEMENTATIONS, (
1118 'codereview {} not in {}'.format(codereview,
1119 _CODEREVIEW_IMPLEMENTATIONS))
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001120 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1121 self._codereview = codereview
1122 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001123 return
1124
1125 # Automatic selection based on issue number set for a current branch.
1126 # Rietveld takes precedence over Gerrit.
1127 assert not self.issue
1128 # Whether we find issue or not, we are doing the lookup.
1129 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001130 if self.GetBranch():
1131 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1132 issue = _git_get_branch_config_value(
1133 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1134 if issue:
1135 self._codereview = codereview
1136 self._codereview_impl = cls(self, **kwargs)
1137 self.issue = int(issue)
1138 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001139
Bryce Thomascfc97122018-12-13 20:21:47 +00001140 # No issue is set for this branch, so default to gerrit.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001141 return self._load_codereview_impl(
Bryce Thomascfc97122018-12-13 20:21:47 +00001142 codereview='gerrit',
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001143 **kwargs)
1144
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001145 def IsGerrit(self):
1146 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001147
1148 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001149 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001150
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001151 The return value is a string suitable for passing to git cl with the --cc
1152 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153 """
1154 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001155 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001156 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001157 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1158 return self.cc
1159
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001160 def GetCCListWithoutDefault(self):
1161 """Return the users cc'd on this CL excluding default ones."""
1162 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001163 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001164 return self.cc
1165
Daniel Cheng7227d212017-11-17 08:12:37 -08001166 def ExtendCC(self, more_cc):
1167 """Extends the list of users to cc on this CL based on the changed files."""
1168 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169
1170 def GetBranch(self):
1171 """Returns the short branch name, e.g. 'master'."""
1172 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001173 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001174 if not branchref:
1175 return None
1176 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 self.branch = ShortBranchName(self.branchref)
1178 return self.branch
1179
1180 def GetBranchRef(self):
1181 """Returns the full branch name, e.g. 'refs/heads/master'."""
1182 self.GetBranch() # Poke the lazy loader.
1183 return self.branchref
1184
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001185 def ClearBranch(self):
1186 """Clears cached branch data of this object."""
1187 self.branch = self.branchref = None
1188
tandrii5d48c322016-08-18 16:19:37 -07001189 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1190 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1191 kwargs['branch'] = self.GetBranch()
1192 return _git_get_branch_config_value(key, default, **kwargs)
1193
1194 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1195 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1196 assert self.GetBranch(), (
1197 'this CL must have an associated branch to %sset %s%s' %
1198 ('un' if value is None else '',
1199 key,
1200 '' if value is None else ' to %r' % value))
1201 kwargs['branch'] = self.GetBranch()
1202 return _git_set_branch_config_value(key, value, **kwargs)
1203
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001204 @staticmethod
1205 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001206 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 e.g. 'origin', 'refs/heads/master'
1208 """
1209 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001210 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1211
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001213 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001215 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1216 error_ok=True).strip()
1217 if upstream_branch:
1218 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001219 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001220 # Else, try to guess the origin remote.
1221 remote_branches = RunGit(['branch', '-r']).split()
1222 if 'origin/master' in remote_branches:
1223 # Fall back on origin/master if it exits.
1224 remote = 'origin'
1225 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001227 DieWithError(
1228 'Unable to determine default branch to diff against.\n'
1229 'Either pass complete "git diff"-style arguments, like\n'
1230 ' git cl upload origin/master\n'
1231 'or verify this branch is set up to track another \n'
1232 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233
1234 return remote, upstream_branch
1235
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001236 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001237 upstream_branch = self.GetUpstreamBranch()
1238 if not BranchExists(upstream_branch):
1239 DieWithError('The upstream for the current branch (%s) does not exist '
1240 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001241 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001242 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001243
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244 def GetUpstreamBranch(self):
1245 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001246 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001248 upstream_branch = upstream_branch.replace('refs/heads/',
1249 'refs/remotes/%s/' % remote)
1250 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1251 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 self.upstream_branch = upstream_branch
1253 return self.upstream_branch
1254
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001255 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001256 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001257 remote, branch = None, self.GetBranch()
1258 seen_branches = set()
1259 while branch not in seen_branches:
1260 seen_branches.add(branch)
1261 remote, branch = self.FetchUpstreamTuple(branch)
1262 branch = ShortBranchName(branch)
1263 if remote != '.' or branch.startswith('refs/remotes'):
1264 break
1265 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001266 remotes = RunGit(['remote'], error_ok=True).split()
1267 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001268 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001269 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001271 logging.warn('Could not determine which remote this change is '
1272 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001273 else:
1274 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001275 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001276 branch = 'HEAD'
1277 if branch.startswith('refs/remotes'):
1278 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001279 elif branch.startswith('refs/branch-heads/'):
1280 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001281 else:
1282 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001283 return self._remote
1284
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001285 def GitSanityChecks(self, upstream_git_obj):
1286 """Checks git repo status and ensures diff is from local commits."""
1287
sbc@chromium.org79706062015-01-14 21:18:12 +00001288 if upstream_git_obj is None:
1289 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001290 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001291 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001292 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001293 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001294 return False
1295
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001296 # Verify the commit we're diffing against is in our current branch.
1297 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1298 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1299 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001300 print('ERROR: %s is not in the current branch. You may need to rebase '
1301 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001302 return False
1303
1304 # List the commits inside the diff, and verify they are all local.
1305 commits_in_diff = RunGit(
1306 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1307 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1308 remote_branch = remote_branch.strip()
1309 if code != 0:
1310 _, remote_branch = self.GetRemoteBranch()
1311
1312 commits_in_remote = RunGit(
1313 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1314
1315 common_commits = set(commits_in_diff) & set(commits_in_remote)
1316 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001317 print('ERROR: Your diff contains %d commits already in %s.\n'
1318 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1319 'the diff. If you are using a custom git flow, you can override'
1320 ' the reference used for this check with "git config '
1321 'gitcl.remotebranch <git-ref>".' % (
1322 len(common_commits), remote_branch, upstream_git_obj),
1323 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001324 return False
1325 return True
1326
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001327 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001328 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001329
1330 Returns None if it is not set.
1331 """
tandrii5d48c322016-08-18 16:19:37 -07001332 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001333
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334 def GetRemoteUrl(self):
1335 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1336
1337 Returns None if there is no remote.
1338 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001339 is_cached, value = self._cached_remote_url
1340 if is_cached:
1341 return value
1342
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001343 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001344 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1345
1346 # If URL is pointing to a local directory, it is probably a git cache.
1347 if os.path.isdir(url):
1348 url = RunGit(['config', 'remote.%s.url' % remote],
1349 error_ok=True,
1350 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001351 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001352 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001354 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001355 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001356 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001357 self.issue = self._GitGetBranchConfigValue(
1358 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001359 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 return self.issue
1361
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 def GetIssueURL(self):
1363 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001364 issue = self.GetIssue()
1365 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001366 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001367 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001368
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001369 def GetDescription(self, pretty=False, force=False):
1370 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001372 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373 self.has_description = True
1374 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001375 # Set width to 72 columns + 2 space indent.
1376 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001378 lines = self.description.splitlines()
1379 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380 return self.description
1381
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001382 def GetDescriptionFooters(self):
1383 """Returns (non_footer_lines, footers) for the commit message.
1384
1385 Returns:
1386 non_footer_lines (list(str)) - Simple list of description lines without
1387 any footer. The lines do not contain newlines, nor does the list contain
1388 the empty line between the message and the footers.
1389 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1390 [("Change-Id", "Ideadbeef...."), ...]
1391 """
1392 raw_description = self.GetDescription()
1393 msg_lines, _, footers = git_footers.split_footers(raw_description)
1394 if footers:
1395 msg_lines = msg_lines[:len(msg_lines)-1]
1396 return msg_lines, footers
1397
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001399 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001400 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001401 self.patchset = self._GitGetBranchConfigValue(
1402 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001403 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 return self.patchset
1405
1406 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001407 """Set this branch's patchset. If patchset=0, clears the patchset."""
1408 assert self.GetBranch()
1409 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001410 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001411 else:
1412 self.patchset = int(patchset)
1413 self._GitSetBranchConfigValue(
1414 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001416 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001417 """Set this branch's issue. If issue isn't given, clears the issue."""
1418 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001420 issue = int(issue)
1421 self._GitSetBranchConfigValue(
1422 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001423 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001424 codereview_server = self._codereview_impl.GetCodereviewServer()
1425 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001426 self._GitSetBranchConfigValue(
1427 self._codereview_impl.CodereviewServerConfigKey(),
1428 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 else:
tandrii5d48c322016-08-18 16:19:37 -07001430 # Reset all of these just to be clean.
1431 reset_suffixes = [
1432 'last-upload-hash',
1433 self._codereview_impl.IssueConfigKey(),
1434 self._codereview_impl.PatchsetConfigKey(),
1435 self._codereview_impl.CodereviewServerConfigKey(),
1436 ] + self._PostUnsetIssueProperties()
1437 for prop in reset_suffixes:
1438 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001439 msg = RunGit(['log', '-1', '--format=%B']).strip()
1440 if msg and git_footers.get_footer_change_id(msg):
1441 print('WARNING: The change patched into this branch has a Change-Id. '
1442 'Removing it.')
1443 RunGit(['commit', '--amend', '-m',
1444 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001445 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001446 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447
dnjba1b0f32016-09-02 12:37:42 -07001448 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001449 if not self.GitSanityChecks(upstream_branch):
1450 DieWithError('\nGit sanity check failure')
1451
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001452 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001453 if not root:
1454 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001455 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001456
1457 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001458 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001459 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001460 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001461 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001462 except subprocess2.CalledProcessError:
1463 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001464 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001465 'This branch probably doesn\'t exist anymore. To reset the\n'
1466 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001467 ' git branch --set-upstream-to origin/master %s\n'
1468 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001469 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001470
maruel@chromium.org52424302012-08-29 15:14:30 +00001471 issue = self.GetIssue()
1472 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001473 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001474 description = self.GetDescription()
1475 else:
1476 # If the change was never uploaded, use the log messages of all commits
1477 # up to the branch point, as git cl upload will prefill the description
1478 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001479 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1480 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001481
1482 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001483 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001484 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001485 name,
1486 description,
1487 absroot,
1488 files,
1489 issue,
1490 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001491 author,
1492 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001493
dsansomee2d6fd92016-09-08 00:10:47 -07001494 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001495 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001496 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001497 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001498
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001499 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1500 """Sets the description for this CL remotely.
1501
1502 You can get description_lines and footers with GetDescriptionFooters.
1503
1504 Args:
1505 description_lines (list(str)) - List of CL description lines without
1506 newline characters.
1507 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1508 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1509 `List-Of-Tokens`). It will be case-normalized so that each token is
1510 title-cased.
1511 """
1512 new_description = '\n'.join(description_lines)
1513 if footers:
1514 new_description += '\n'
1515 for k, v in footers:
1516 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1517 if not git_footers.FOOTER_PATTERN.match(foot):
1518 raise ValueError('Invalid footer %r' % foot)
1519 new_description += foot + '\n'
1520 self.UpdateDescription(new_description, force)
1521
Edward Lesmes8e282792018-04-03 18:50:29 -04001522 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001523 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1524 try:
1525 return presubmit_support.DoPresubmitChecks(change, committing,
1526 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1527 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001528 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1529 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001530 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001531 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001532
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001533 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1534 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001535 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1536 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001537 else:
1538 # Assume url.
1539 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1540 urlparse.urlparse(issue_arg))
1541 if not parsed_issue_arg or not parsed_issue_arg.valid:
1542 DieWithError('Failed to parse issue argument "%s". '
1543 'Must be an issue number or a valid URL.' % issue_arg)
1544 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001545 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001546
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001547 def CMDUpload(self, options, git_diff_args, orig_args):
1548 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001549 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001550 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001551 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001552 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001553 else:
1554 if self.GetBranch() is None:
1555 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1556
1557 # Default to diffing against common ancestor of upstream branch
1558 base_branch = self.GetCommonAncestorWithUpstream()
1559 git_diff_args = [base_branch, 'HEAD']
1560
Aaron Gablec4c40d12017-05-22 11:49:53 -07001561
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001562 # Fast best-effort checks to abort before running potentially
1563 # expensive hooks if uploading is likely to fail anyway. Passing these
1564 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001565 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001566 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001567
1568 # Apply watchlists on upload.
1569 change = self.GetChange(base_branch, None)
1570 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1571 files = [f.LocalPath() for f in change.AffectedFiles()]
1572 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001573 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001574
1575 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001576 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001577 # Set the reviewer list now so that presubmit checks can access it.
1578 change_description = ChangeDescription(change.FullDescriptionText())
1579 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001580 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001581 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001582 change)
1583 change.SetDescriptionText(change_description.description)
1584 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001585 may_prompt=not options.force,
1586 verbose=options.verbose,
1587 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001588 if not hook_results.should_continue():
1589 return 1
1590 if not options.reviewers and hook_results.reviewers:
1591 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001592 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001593
Aaron Gable13101a62018-02-09 13:20:41 -08001594 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001595 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001596 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001597 _git_set_branch_config_value('last-upload-hash',
1598 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001599 # Run post upload hooks, if specified.
1600 if settings.GetRunPostUploadHook():
1601 presubmit_support.DoPostUploadExecuter(
1602 change,
1603 self,
1604 settings.GetRoot(),
1605 options.verbose,
1606 sys.stdout)
1607
1608 # Upload all dependencies if specified.
1609 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001610 print()
1611 print('--dependencies has been specified.')
1612 print('All dependent local branches will be re-uploaded.')
1613 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001614 # Remove the dependencies flag from args so that we do not end up in a
1615 # loop.
1616 orig_args.remove('--dependencies')
1617 ret = upload_branch_deps(self, orig_args)
1618 return ret
1619
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001620 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001621 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001622
1623 Issue must have been already uploaded and known.
1624 """
1625 assert new_state in _CQState.ALL_STATES
1626 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001627 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001628 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001629 return 0
1630 except KeyboardInterrupt:
1631 raise
1632 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001633 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001634 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001635 ' * Your project has no CQ,\n'
1636 ' * You don\'t have permission to change the CQ state,\n'
1637 ' * There\'s a bug in this code (see stack trace below).\n'
1638 'Consider specifying which bots to trigger manually or asking your '
1639 'project owners for permissions or contacting Chrome Infra at:\n'
1640 'https://www.chromium.org/infra\n\n' %
1641 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001642 # Still raise exception so that stack trace is printed.
1643 raise
1644
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001645 # Forward methods to codereview specific implementation.
1646
Aaron Gable636b13f2017-07-14 10:42:48 -07001647 def AddComment(self, message, publish=None):
1648 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001649
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001650 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001651 """Returns list of _CommentSummary for each comment.
1652
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001653 args:
1654 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001655 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001656 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001657
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001658 def CloseIssue(self):
1659 return self._codereview_impl.CloseIssue()
1660
1661 def GetStatus(self):
1662 return self._codereview_impl.GetStatus()
1663
1664 def GetCodereviewServer(self):
1665 return self._codereview_impl.GetCodereviewServer()
1666
tandriide281ae2016-10-12 06:02:30 -07001667 def GetIssueOwner(self):
1668 """Get owner from codereview, which may differ from this checkout."""
1669 return self._codereview_impl.GetIssueOwner()
1670
Edward Lemur707d70b2018-02-07 00:50:14 +01001671 def GetReviewers(self):
1672 return self._codereview_impl.GetReviewers()
1673
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001674 def GetMostRecentPatchset(self):
1675 return self._codereview_impl.GetMostRecentPatchset()
1676
tandriide281ae2016-10-12 06:02:30 -07001677 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001678 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001679 return self._codereview_impl.CannotTriggerTryJobReason()
1680
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001681 def GetTryJobProperties(self, patchset=None):
1682 """Returns dictionary of properties to launch try job."""
1683 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001684
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001685 def __getattr__(self, attr):
1686 # This is because lots of untested code accesses Rietveld-specific stuff
1687 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001688 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001689 # Note that child method defines __getattr__ as well, and forwards it here,
1690 # because _RietveldChangelistImpl is not cleaned up yet, and given
1691 # deprecation of Rietveld, it should probably be just removed.
1692 # Until that time, avoid infinite recursion by bypassing __getattr__
1693 # of implementation class.
1694 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001695
1696
1697class _ChangelistCodereviewBase(object):
1698 """Abstract base class encapsulating codereview specifics of a changelist."""
1699 def __init__(self, changelist):
1700 self._changelist = changelist # instance of Changelist
1701
1702 def __getattr__(self, attr):
1703 # Forward methods to changelist.
1704 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1705 # _RietveldChangelistImpl to avoid this hack?
1706 return getattr(self._changelist, attr)
1707
1708 def GetStatus(self):
1709 """Apply a rough heuristic to give a simple summary of an issue's review
1710 or CQ status, assuming adherence to a common workflow.
1711
1712 Returns None if no issue for this branch, or specific string keywords.
1713 """
1714 raise NotImplementedError()
1715
1716 def GetCodereviewServer(self):
1717 """Returns server URL without end slash, like "https://codereview.com"."""
1718 raise NotImplementedError()
1719
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001720 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001721 """Fetches and returns description from the codereview server."""
1722 raise NotImplementedError()
1723
tandrii5d48c322016-08-18 16:19:37 -07001724 @classmethod
1725 def IssueConfigKey(cls):
1726 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001727 raise NotImplementedError()
1728
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001729 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001730 def PatchsetConfigKey(cls):
1731 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001732 raise NotImplementedError()
1733
tandrii5d48c322016-08-18 16:19:37 -07001734 @classmethod
1735 def CodereviewServerConfigKey(cls):
1736 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001737 raise NotImplementedError()
1738
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001739 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001740 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001741 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001742
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001743 def GetGerritObjForPresubmit(self):
1744 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1745 return None
1746
dsansomee2d6fd92016-09-08 00:10:47 -07001747 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001748 """Update the description on codereview site."""
1749 raise NotImplementedError()
1750
Aaron Gable636b13f2017-07-14 10:42:48 -07001751 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001752 """Posts a comment to the codereview site."""
1753 raise NotImplementedError()
1754
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001755 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001756 raise NotImplementedError()
1757
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001758 def CloseIssue(self):
1759 """Closes the issue."""
1760 raise NotImplementedError()
1761
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001762 def GetMostRecentPatchset(self):
1763 """Returns the most recent patchset number from the codereview site."""
1764 raise NotImplementedError()
1765
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001766 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001767 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001768 """Fetches and applies the issue.
1769
1770 Arguments:
1771 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1772 reject: if True, reject the failed patch instead of switching to 3-way
1773 merge. Rietveld only.
1774 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1775 only.
1776 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001777 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001778 """
1779 raise NotImplementedError()
1780
1781 @staticmethod
1782 def ParseIssueURL(parsed_url):
1783 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1784 failed."""
1785 raise NotImplementedError()
1786
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001787 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001788 """Best effort check that user is authenticated with codereview server.
1789
1790 Arguments:
1791 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001792 refresh: whether to attempt to refresh credentials. Ignored if not
1793 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001794 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001795 raise NotImplementedError()
1796
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001797 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001798 """Best effort check that uploading isn't supposed to fail for predictable
1799 reasons.
1800
1801 This method should raise informative exception if uploading shouldn't
1802 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001803
1804 Arguments:
1805 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001806 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001807 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001808
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001809 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001810 """Uploads a change to codereview."""
1811 raise NotImplementedError()
1812
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001813 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001814 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001815
1816 Issue must have been already uploaded and known.
1817 """
1818 raise NotImplementedError()
1819
tandriie113dfd2016-10-11 10:20:12 -07001820 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001821 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001822 raise NotImplementedError()
1823
tandriide281ae2016-10-12 06:02:30 -07001824 def GetIssueOwner(self):
1825 raise NotImplementedError()
1826
Edward Lemur707d70b2018-02-07 00:50:14 +01001827 def GetReviewers(self):
1828 raise NotImplementedError()
1829
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001830 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001831 raise NotImplementedError()
1832
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001833
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001834class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001835 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001836 # auth_config is Rietveld thing, kept here to preserve interface only.
1837 super(_GerritChangelistImpl, self).__init__(changelist)
1838 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001839 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001840 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001841 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001842 # Map from change number (issue) to its detail cache.
1843 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001844
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001845 if codereview_host is not None:
1846 assert not codereview_host.startswith('https://'), codereview_host
1847 self._gerrit_host = codereview_host
1848 self._gerrit_server = 'https://%s' % codereview_host
1849
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001850 def _GetGerritHost(self):
1851 # Lazy load of configs.
1852 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001853 if self._gerrit_host and '.' not in self._gerrit_host:
1854 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1855 # This happens for internal stuff http://crbug.com/614312.
1856 parsed = urlparse.urlparse(self.GetRemoteUrl())
1857 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001858 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07001859 ' Your current remote is: %s' % self.GetRemoteUrl())
1860 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1861 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001862 return self._gerrit_host
1863
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001864 def _GetGitHost(self):
1865 """Returns git host to be used when uploading change to Gerrit."""
1866 return urlparse.urlparse(self.GetRemoteUrl()).netloc
1867
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001868 def GetCodereviewServer(self):
1869 if not self._gerrit_server:
1870 # If we're on a branch then get the server potentially associated
1871 # with that branch.
1872 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001873 self._gerrit_server = self._GitGetBranchConfigValue(
1874 self.CodereviewServerConfigKey())
1875 if self._gerrit_server:
1876 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001877 if not self._gerrit_server:
1878 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1879 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001880 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001881 parts[0] = parts[0] + '-review'
1882 self._gerrit_host = '.'.join(parts)
1883 self._gerrit_server = 'https://%s' % self._gerrit_host
1884 return self._gerrit_server
1885
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001886 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001887 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001888 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001889 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001890 logging.warn('can\'t detect Gerrit project.')
1891 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001892 project = urlparse.urlparse(remote_url).path.strip('/')
1893 if project.endswith('.git'):
1894 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001895 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1896 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1897 # gitiles/git-over-https protocol. E.g.,
1898 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1899 # as
1900 # https://chromium.googlesource.com/v8/v8
1901 if project.startswith('a/'):
1902 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001903 return project
1904
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001905 def _GerritChangeIdentifier(self):
1906 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1907
1908 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001909 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001910 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001911 project = self._GetGerritProject()
1912 if project:
1913 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1914 # Fall back on still unique, but less efficient change number.
1915 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001916
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001917 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001918 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001919 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920
tandrii5d48c322016-08-18 16:19:37 -07001921 @classmethod
1922 def PatchsetConfigKey(cls):
1923 return 'gerritpatchset'
1924
1925 @classmethod
1926 def CodereviewServerConfigKey(cls):
1927 return 'gerritserver'
1928
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001929 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001930 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001931 if settings.GetGerritSkipEnsureAuthenticated():
1932 # For projects with unusual authentication schemes.
1933 # See http://crbug.com/603378.
1934 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001935
1936 # Check presence of cookies only if using cookies-based auth method.
1937 cookie_auth = gerrit_util.Authenticator.get()
1938 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001939 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001940
1941 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001942 self.GetCodereviewServer()
1943 git_host = self._GetGitHost()
1944 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001945
1946 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1947 git_auth = cookie_auth.get_auth_header(git_host)
1948 if gerrit_auth and git_auth:
1949 if gerrit_auth == git_auth:
1950 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001951 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001952 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001953 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001954 ' %s\n'
1955 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001956 ' Consider running the following command:\n'
1957 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001958 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02001959 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001960 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001961 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001962 cookie_auth.get_new_password_message(git_host)))
1963 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001964 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001965 return
1966 else:
1967 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001968 ([] if gerrit_auth else [self._gerrit_host]) +
1969 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001970 DieWithError('Credentials for the following hosts are required:\n'
1971 ' %s\n'
1972 'These are read from %s (or legacy %s)\n'
1973 '%s' % (
1974 '\n '.join(missing),
1975 cookie_auth.get_gitcookies_path(),
1976 cookie_auth.get_netrc_path(),
1977 cookie_auth.get_new_password_message(git_host)))
1978
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001979 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001980 if not self.GetIssue():
1981 return
1982
1983 # Warm change details cache now to avoid RPCs later, reducing latency for
1984 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001985 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00001986 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001987
1988 status = self._GetChangeDetail()['status']
1989 if status in ('MERGED', 'ABANDONED'):
1990 DieWithError('Change %s has been %s, new uploads are not allowed' %
1991 (self.GetIssueURL(),
1992 'submitted' if status == 'MERGED' else 'abandoned'))
1993
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001994 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1995 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1996 # Apparently this check is not very important? Otherwise get_auth_email
1997 # could have been added to other implementations of Authenticator.
1998 cookies_auth = gerrit_util.Authenticator.get()
1999 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002000 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002001
2002 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002003 if self.GetIssueOwner() == cookies_user:
2004 return
2005 logging.debug('change %s owner is %s, cookies user is %s',
2006 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002007 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002008 # so ask what Gerrit thinks of this user.
2009 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2010 if details['email'] == self.GetIssueOwner():
2011 return
2012 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002013 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002014 'as %s.\n'
2015 'Uploading may fail due to lack of permissions.' %
2016 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2017 confirm_or_exit(action='upload')
2018
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002019 def _PostUnsetIssueProperties(self):
2020 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002021 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002022
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002023 def GetGerritObjForPresubmit(self):
2024 return presubmit_support.GerritAccessor(self._GetGerritHost())
2025
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002026 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002027 """Apply a rough heuristic to give a simple summary of an issue's review
2028 or CQ status, assuming adherence to a common workflow.
2029
2030 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002031 * 'error' - error from review tool (including deleted issues)
2032 * 'unsent' - no reviewers added
2033 * 'waiting' - waiting for review
2034 * 'reply' - waiting for uploader to reply to review
2035 * 'lgtm' - Code-Review label has been set
2036 * 'commit' - in the commit queue
2037 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002038 """
2039 if not self.GetIssue():
2040 return None
2041
2042 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002043 data = self._GetChangeDetail([
2044 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002045 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002046 return 'error'
2047
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002048 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002049 return 'closed'
2050
Aaron Gable9ab38c62017-04-06 14:36:33 -07002051 if data['labels'].get('Commit-Queue', {}).get('approved'):
2052 # The section will have an "approved" subsection if anyone has voted
2053 # the maximum value on the label.
2054 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002055
Aaron Gable9ab38c62017-04-06 14:36:33 -07002056 if data['labels'].get('Code-Review', {}).get('approved'):
2057 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002058
2059 if not data.get('reviewers', {}).get('REVIEWER', []):
2060 return 'unsent'
2061
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002062 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002063 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2064 last_message_author = messages.pop().get('author', {})
2065 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002066 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2067 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002068 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002069 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002070 if last_message_author.get('_account_id') == owner:
2071 # Most recent message was by owner.
2072 return 'waiting'
2073 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002074 # Some reply from non-owner.
2075 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002076
2077 # Somehow there are no messages even though there are reviewers.
2078 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002079
2080 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002081 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002082 patchset = data['revisions'][data['current_revision']]['_number']
2083 self.SetPatchset(patchset)
2084 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002085
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002086 def FetchDescription(self, force=False):
2087 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2088 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002089 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002090 return data['revisions'][current_rev]['commit']['message'].encode(
2091 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002092
dsansomee2d6fd92016-09-08 00:10:47 -07002093 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002094 if gerrit_util.HasPendingChangeEdit(
2095 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002096 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002097 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002098 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002099 'unpublished edit. Either publish the edit in the Gerrit web UI '
2100 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002101
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002102 gerrit_util.DeletePendingChangeEdit(
2103 self._GetGerritHost(), self._GerritChangeIdentifier())
2104 gerrit_util.SetCommitMessage(
2105 self._GetGerritHost(), self._GerritChangeIdentifier(),
2106 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002107
Aaron Gable636b13f2017-07-14 10:42:48 -07002108 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002109 gerrit_util.SetReview(
2110 self._GetGerritHost(), self._GerritChangeIdentifier(),
2111 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002112
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002113 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002114 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002115 # CURRENT_REVISION is included to get the latest patchset so that
2116 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002117 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002118 options=['MESSAGES', 'DETAILED_ACCOUNTS',
2119 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002120 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002121 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002122 robot_file_comments = gerrit_util.GetChangeRobotComments(
2123 self._GetGerritHost(), self._GerritChangeIdentifier())
2124
2125 # Add the robot comments onto the list of comments, but only
2126 # keep those that are from the latest pachset.
2127 latest_patch_set = self.GetMostRecentPatchset()
2128 for path, robot_comments in robot_file_comments.iteritems():
2129 line_comments = file_comments.setdefault(path, [])
2130 line_comments.extend(
2131 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002132
2133 # Build dictionary of file comments for easy access and sorting later.
2134 # {author+date: {path: {patchset: {line: url+message}}}}
2135 comments = collections.defaultdict(
2136 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2137 for path, line_comments in file_comments.iteritems():
2138 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002139 tag = comment.get('tag', '')
2140 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002141 continue
2142 key = (comment['author']['email'], comment['updated'])
2143 if comment.get('side', 'REVISION') == 'PARENT':
2144 patchset = 'Base'
2145 else:
2146 patchset = 'PS%d' % comment['patch_set']
2147 line = comment.get('line', 0)
2148 url = ('https://%s/c/%s/%s/%s#%s%s' %
2149 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2150 'b' if comment.get('side') == 'PARENT' else '',
2151 str(line) if line else ''))
2152 comments[key][path][patchset][line] = (url, comment['message'])
2153
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002154 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002155 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002156 summary = self._BuildCommentSummary(msg, comments, readable)
2157 if summary:
2158 summaries.append(summary)
2159 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002160
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002161 @staticmethod
2162 def _BuildCommentSummary(msg, comments, readable):
2163 key = (msg['author']['email'], msg['date'])
2164 # Don't bother showing autogenerated messages that don't have associated
2165 # file or line comments. this will filter out most autogenerated
2166 # messages, but will keep robot comments like those from Tricium.
2167 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2168 if is_autogenerated and not comments.get(key):
2169 return None
2170 message = msg['message']
2171 # Gerrit spits out nanoseconds.
2172 assert len(msg['date'].split('.')[-1]) == 9
2173 date = datetime.datetime.strptime(msg['date'][:-3],
2174 '%Y-%m-%d %H:%M:%S.%f')
2175 if key in comments:
2176 message += '\n'
2177 for path, patchsets in sorted(comments.get(key, {}).items()):
2178 if readable:
2179 message += '\n%s' % path
2180 for patchset, lines in sorted(patchsets.items()):
2181 for line, (url, content) in sorted(lines.items()):
2182 if line:
2183 line_str = 'Line %d' % line
2184 path_str = '%s:%d:' % (path, line)
2185 else:
2186 line_str = 'File comment'
2187 path_str = '%s:0:' % path
2188 if readable:
2189 message += '\n %s, %s: %s' % (patchset, line_str, url)
2190 message += '\n %s\n' % content
2191 else:
2192 message += '\n%s ' % path_str
2193 message += '\n%s\n' % content
2194
2195 return _CommentSummary(
2196 date=date,
2197 message=message,
2198 sender=msg['author']['email'],
2199 autogenerated=is_autogenerated,
2200 # These could be inferred from the text messages and correlated with
2201 # Code-Review label maximum, however this is not reliable.
2202 # Leaving as is until the need arises.
2203 approval=False,
2204 disapproval=False,
2205 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002206
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002207 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002208 gerrit_util.AbandonChange(
2209 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002210
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002211 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002212 gerrit_util.SubmitChange(
2213 self._GetGerritHost(), self._GerritChangeIdentifier(),
2214 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002215
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002216 def _GetChangeDetail(self, options=None, no_cache=False):
2217 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002218
2219 If fresh data is needed, set no_cache=True which will clear cache and
2220 thus new data will be fetched from Gerrit.
2221 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002222 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002223 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002224
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002225 # Optimization to avoid multiple RPCs:
2226 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2227 'CURRENT_COMMIT' not in options):
2228 options.append('CURRENT_COMMIT')
2229
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002230 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002231 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002232 options = [o.upper() for o in options]
2233
2234 # Check in cache first unless no_cache is True.
2235 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002236 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002237 else:
2238 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002239 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002240 # Assumption: data fetched before with extra options is suitable
2241 # for return for a smaller set of options.
2242 # For example, if we cached data for
2243 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2244 # and request is for options=[CURRENT_REVISION],
2245 # THEN we can return prior cached data.
2246 if options_set.issubset(cached_options_set):
2247 return data
2248
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002249 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002250 data = gerrit_util.GetChangeDetail(
2251 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002252 except gerrit_util.GerritError as e:
2253 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002254 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002255 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002256
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002257 self._detail_cache.setdefault(cache_key, []).append(
2258 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002259 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002260
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002261 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002262 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002263 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002264 data = gerrit_util.GetChangeCommit(
2265 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002266 except gerrit_util.GerritError as e:
2267 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002268 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002269 raise
agable32978d92016-11-01 12:55:02 -07002270 return data
2271
Olivier Robin75ee7252018-04-13 10:02:56 +02002272 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002273 if git_common.is_dirty_git_tree('land'):
2274 return 1
tandriid60367b2016-06-22 05:25:12 -07002275 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2276 if u'Commit-Queue' in detail.get('labels', {}):
2277 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002278 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2279 'which can test and land changes for you. '
2280 'Are you sure you wish to bypass it?\n',
2281 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002282
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002283 differs = True
tandriic4344b52016-08-29 06:04:54 -07002284 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002285 # Note: git diff outputs nothing if there is no diff.
2286 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002287 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002288 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002289 if detail['current_revision'] == last_upload:
2290 differs = False
2291 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002292 print('WARNING: Local branch contents differ from latest uploaded '
2293 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002294 if differs:
2295 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002296 confirm_or_exit(
2297 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2298 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002299 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002300 elif not bypass_hooks:
2301 hook_results = self.RunHook(
2302 committing=True,
2303 may_prompt=not force,
2304 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002305 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2306 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002307 if not hook_results.should_continue():
2308 return 1
2309
2310 self.SubmitIssue(wait_for_merge=True)
2311 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002312 links = self._GetChangeCommit().get('web_links', [])
2313 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002314 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002315 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002316 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002317 return 0
2318
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002319 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002320 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002321 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002322 assert not directory
2323 assert parsed_issue_arg.valid
2324
2325 self._changelist.issue = parsed_issue_arg.issue
2326
2327 if parsed_issue_arg.hostname:
2328 self._gerrit_host = parsed_issue_arg.hostname
2329 self._gerrit_server = 'https://%s' % self._gerrit_host
2330
tandriic2405f52016-10-10 08:13:15 -07002331 try:
2332 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002333 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002334 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002335
2336 if not parsed_issue_arg.patchset:
2337 # Use current revision by default.
2338 revision_info = detail['revisions'][detail['current_revision']]
2339 patchset = int(revision_info['_number'])
2340 else:
2341 patchset = parsed_issue_arg.patchset
2342 for revision_info in detail['revisions'].itervalues():
2343 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2344 break
2345 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002346 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002347 (parsed_issue_arg.patchset, self.GetIssue()))
2348
Aaron Gable697a91b2018-01-19 15:20:15 -08002349 remote_url = self._changelist.GetRemoteUrl()
2350 if remote_url.endswith('.git'):
2351 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002352 remote_url = remote_url.rstrip('/')
2353
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002354 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002355 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002356
2357 if remote_url != fetch_info['url']:
2358 DieWithError('Trying to patch a change from %s but this repo appears '
2359 'to be %s.' % (fetch_info['url'], remote_url))
2360
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002361 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002362
Aaron Gable62619a32017-06-16 08:22:09 -07002363 if force:
2364 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2365 print('Checked out commit for change %i patchset %i locally' %
2366 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002367 elif nocommit:
2368 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2369 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002370 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002371 RunGit(['cherry-pick', 'FETCH_HEAD'])
2372 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002373 (parsed_issue_arg.issue, patchset))
2374 print('Note: this created a local commit which does not have '
2375 'the same hash as the one uploaded for review. This will make '
2376 'uploading changes based on top of this branch difficult.\n'
2377 'If you want to do that, use "git cl patch --force" instead.')
2378
Stefan Zagerd08043c2017-10-12 12:07:02 -07002379 if self.GetBranch():
2380 self.SetIssue(parsed_issue_arg.issue)
2381 self.SetPatchset(patchset)
2382 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2383 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2384 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2385 else:
2386 print('WARNING: You are in detached HEAD state.\n'
2387 'The patch has been applied to your checkout, but you will not be '
2388 'able to upload a new patch set to the gerrit issue.\n'
2389 'Try using the \'-b\' option if you would like to work on a '
2390 'branch and/or upload a new patch set.')
2391
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002392 return 0
2393
2394 @staticmethod
2395 def ParseIssueURL(parsed_url):
2396 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2397 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002398 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2399 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002400 # Short urls like https://domain/<issue_number> can be used, but don't allow
2401 # specifying the patchset (you'd 404), but we allow that here.
2402 if parsed_url.path == '/':
2403 part = parsed_url.fragment
2404 else:
2405 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002406 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002407 if match:
2408 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002409 issue=int(match.group(3)),
2410 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002411 hostname=parsed_url.netloc,
2412 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002413 return None
2414
tandrii16e0b4e2016-06-07 10:34:28 -07002415 def _GerritCommitMsgHookCheck(self, offer_removal):
2416 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2417 if not os.path.exists(hook):
2418 return
2419 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2420 # custom developer made one.
2421 data = gclient_utils.FileRead(hook)
2422 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2423 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002424 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002425 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002426 'and may interfere with it in subtle ways.\n'
2427 'We recommend you remove the commit-msg hook.')
2428 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002429 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002430 gclient_utils.rm_file_or_tree(hook)
2431 print('Gerrit commit-msg hook removed.')
2432 else:
2433 print('OK, will keep Gerrit commit-msg hook in place.')
2434
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002435 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002436 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002437 if options.squash and options.no_squash:
2438 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002439
2440 if not options.squash and not options.no_squash:
2441 # Load default for user, repo, squash=true, in this order.
2442 options.squash = settings.GetSquashGerritUploads()
2443 elif options.no_squash:
2444 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002445
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002446 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002447 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002448
Aaron Gableb56ad332017-01-06 15:24:31 -08002449 # This may be None; default fallback value is determined in logic below.
2450 title = options.title
2451
Dominic Battre7d1c4842017-10-27 09:17:28 +02002452 # Extract bug number from branch name.
2453 bug = options.bug
2454 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2455 if not bug and match:
2456 bug = match.group(1)
2457
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002458 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002459 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002460 if self.GetIssue():
2461 # Try to get the message from a previous upload.
2462 message = self.GetDescription()
2463 if not message:
2464 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002465 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002466 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002467 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002468 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002469 # When uploading a subsequent patchset, -m|--message is taken
2470 # as the patchset title if --title was not provided.
2471 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002472 else:
2473 default_title = RunGit(
2474 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002475 if options.force:
2476 title = default_title
2477 else:
2478 title = ask_for_data(
2479 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002480 change_id = self._GetChangeDetail()['change_id']
2481 while True:
2482 footer_change_ids = git_footers.get_footer_change_id(message)
2483 if footer_change_ids == [change_id]:
2484 break
2485 if not footer_change_ids:
2486 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002487 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002488 continue
2489 # There is already a valid footer but with different or several ids.
2490 # Doing this automatically is non-trivial as we don't want to lose
2491 # existing other footers, yet we want to append just 1 desired
2492 # Change-Id. Thus, just create a new footer, but let user verify the
2493 # new description.
2494 message = '%s\n\nChange-Id: %s' % (message, change_id)
2495 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002496 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002497 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002498 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002499 'Please, check the proposed correction to the description, '
2500 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2501 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2502 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002503 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002504 if not options.force:
2505 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002506 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002507 message = change_desc.description
2508 if not message:
2509 DieWithError("Description is empty. Aborting...")
2510 # Continue the while loop.
2511 # Sanity check of this code - we should end up with proper message
2512 # footer.
2513 assert [change_id] == git_footers.get_footer_change_id(message)
2514 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002515 else: # if not self.GetIssue()
2516 if options.message:
2517 message = options.message
2518 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002519 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002520 if options.title:
2521 message = options.title + '\n\n' + message
2522 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002523
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002524 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002525 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002526 # On first upload, patchset title is always this string, while
2527 # --title flag gets converted to first line of message.
2528 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002529 if not change_desc.description:
2530 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002531 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002532 if len(change_ids) > 1:
2533 DieWithError('too many Change-Id footers, at most 1 allowed.')
2534 if not change_ids:
2535 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002536 change_desc.set_description(git_footers.add_footer_change_id(
2537 change_desc.description,
2538 GenerateGerritChangeId(change_desc.description)))
2539 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002540 assert len(change_ids) == 1
2541 change_id = change_ids[0]
2542
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002543 if options.reviewers or options.tbrs or options.add_owners_to:
2544 change_desc.update_reviewers(options.reviewers, options.tbrs,
2545 options.add_owners_to, change)
2546
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002547 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002548 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2549 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002550 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002551 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2552 desc_tempfile.write(change_desc.description)
2553 desc_tempfile.close()
2554 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2555 '-F', desc_tempfile.name]).strip()
2556 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002557 else:
2558 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002559 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002560 if not change_desc.description:
2561 DieWithError("Description is empty. Aborting...")
2562
2563 if not git_footers.get_footer_change_id(change_desc.description):
2564 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002565 change_desc.set_description(
2566 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002567 if options.reviewers or options.tbrs or options.add_owners_to:
2568 change_desc.update_reviewers(options.reviewers, options.tbrs,
2569 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002570 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002571 # For no-squash mode, we assume the remote called "origin" is the one we
2572 # want. It is not worthwhile to support different workflows for
2573 # no-squash mode.
2574 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002575 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2576
2577 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002578 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002579 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2580 ref_to_push)]).splitlines()
2581 if len(commits) > 1:
2582 print('WARNING: This will upload %d commits. Run the following command '
2583 'to see which commits will be uploaded: ' % len(commits))
2584 print('git log %s..%s' % (parent, ref_to_push))
2585 print('You can also use `git squash-branch` to squash these into a '
2586 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002587 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002588
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002589 if options.reviewers or options.tbrs or options.add_owners_to:
2590 change_desc.update_reviewers(options.reviewers, options.tbrs,
2591 options.add_owners_to, change)
2592
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002593 reviewers = sorted(change_desc.get_reviewers())
2594 # Add cc's from the CC_LIST and --cc flag (if any).
2595 if not options.private and not options.no_autocc:
2596 cc = self.GetCCList().split(',')
2597 else:
2598 cc = []
2599 if options.cc:
2600 cc.extend(options.cc)
2601 cc = filter(None, [email.strip() for email in cc])
2602 if change_desc.get_cced():
2603 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002604 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2605 valid_accounts = set(reviewers + cc)
2606 # TODO(crbug/877717): relax this for all hosts.
2607 else:
2608 valid_accounts = gerrit_util.ValidAccounts(
2609 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002610 logging.info('accounts %s are recognized, %s invalid',
2611 sorted(valid_accounts),
2612 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002613
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002614 # Extra options that can be specified at push time. Doc:
2615 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002616 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002617
Aaron Gable844cf292017-06-28 11:32:59 -07002618 # By default, new changes are started in WIP mode, and subsequent patchsets
2619 # don't send email. At any time, passing --send-mail will mark the change
2620 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002621 if options.send_mail:
2622 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002623 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002624 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002625 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002626 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002627 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002628
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002629 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002630 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002631
Aaron Gable9b713dd2016-12-14 16:04:21 -08002632 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002633 # Punctuation and whitespace in |title| must be percent-encoded.
2634 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002635
agablec6787972016-09-09 16:13:34 -07002636 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002637 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002638
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002639 for r in sorted(reviewers):
2640 if r in valid_accounts:
2641 refspec_opts.append('r=%s' % r)
2642 reviewers.remove(r)
2643 else:
2644 # TODO(tandrii): this should probably be a hard failure.
2645 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2646 % r)
2647 for c in sorted(cc):
2648 # refspec option will be rejected if cc doesn't correspond to an
2649 # account, even though REST call to add such arbitrary cc may succeed.
2650 if c in valid_accounts:
2651 refspec_opts.append('cc=%s' % c)
2652 cc.remove(c)
2653
rmistry9eadede2016-09-19 11:22:43 -07002654 if options.topic:
2655 # Documentation on Gerrit topics is here:
2656 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002657 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002658
Edward Lemur687ca902018-12-05 02:30:30 +00002659 if options.enable_auto_submit:
2660 refspec_opts.append('l=Auto-Submit+1')
2661 if options.use_commit_queue:
2662 refspec_opts.append('l=Commit-Queue+2')
2663 elif options.cq_dry_run:
2664 refspec_opts.append('l=Commit-Queue+1')
2665
2666 if change_desc.get_reviewers(tbr_only=True):
2667 score = gerrit_util.GetCodeReviewTbrScore(
2668 self._GetGerritHost(),
2669 self._GetGerritProject())
2670 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002671
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002672 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002673 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002674 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002675 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002676 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2677
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002678 refspec_suffix = ''
2679 if refspec_opts:
2680 refspec_suffix = '%' + ','.join(refspec_opts)
2681 assert ' ' not in refspec_suffix, (
2682 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2683 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2684
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002685 try:
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002686 before_push = time_time()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002687 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00002688 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemuredcefdc2018-11-08 14:41:42 +00002689 print_stdout=True,
Edward Lemur49c8eaf2018-11-07 22:13:12 +00002690 # Flush after every line: useful for seeing progress when running as
2691 # recipe.
2692 filter_fn=lambda _: sys.stdout.flush())
2693 push_returncode = 0
Edward Lemurfec80c42018-11-01 23:14:14 +00002694 except subprocess2.CalledProcessError as e:
2695 push_returncode = e.returncode
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002696 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002697 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002698 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002699 'credential problems:\n'
2700 ' git cl creds-check\n',
2701 change_desc)
Edward Lemurfec80c42018-11-01 23:14:14 +00002702 finally:
2703 metrics.collector.add_repeated('sub_commands', {
2704 'command': 'git push',
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002705 'execution_time': time_time() - before_push,
Edward Lemurfec80c42018-11-01 23:14:14 +00002706 'exit_code': push_returncode,
2707 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2708 })
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002709
2710 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002711 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002712 change_numbers = [m.group(1)
2713 for m in map(regex.match, push_stdout.splitlines())
2714 if m]
2715 if len(change_numbers) != 1:
2716 DieWithError(
2717 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002718 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002719 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002720 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002721
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002722 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002723 # GetIssue() is not set in case of non-squash uploads according to tests.
2724 # TODO(agable): non-squash uploads in git cl should be removed.
2725 gerrit_util.AddReviewers(
2726 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002727 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002728 reviewers, cc,
2729 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002730
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002731 return 0
2732
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002733 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2734 change_desc):
2735 """Computes parent of the generated commit to be uploaded to Gerrit.
2736
2737 Returns revision or a ref name.
2738 """
2739 if custom_cl_base:
2740 # Try to avoid creating additional unintended CLs when uploading, unless
2741 # user wants to take this risk.
2742 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2743 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2744 local_ref_of_target_remote])
2745 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002746 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002747 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2748 'If you proceed with upload, more than 1 CL may be created by '
2749 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2750 'If you are certain that specified base `%s` has already been '
2751 'uploaded to Gerrit as another CL, you may proceed.\n' %
2752 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2753 if not force:
2754 confirm_or_exit(
2755 'Do you take responsibility for cleaning up potential mess '
2756 'resulting from proceeding with upload?',
2757 action='upload')
2758 return custom_cl_base
2759
Aaron Gablef97e33d2017-03-30 15:44:27 -07002760 if remote != '.':
2761 return self.GetCommonAncestorWithUpstream()
2762
2763 # If our upstream branch is local, we base our squashed commit on its
2764 # squashed version.
2765 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2766
Aaron Gablef97e33d2017-03-30 15:44:27 -07002767 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002768 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002769
2770 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002771 # TODO(tandrii): consider checking parent change in Gerrit and using its
2772 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2773 # the tree hash of the parent branch. The upside is less likely bogus
2774 # requests to reupload parent change just because it's uploadhash is
2775 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002776 parent = RunGit(['config',
2777 'branch.%s.gerritsquashhash' % upstream_branch_name],
2778 error_ok=True).strip()
2779 # Verify that the upstream branch has been uploaded too, otherwise
2780 # Gerrit will create additional CLs when uploading.
2781 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2782 RunGitSilent(['rev-parse', parent + ':'])):
2783 DieWithError(
2784 '\nUpload upstream branch %s first.\n'
2785 'It is likely that this branch has been rebased since its last '
2786 'upload, so you just need to upload it again.\n'
2787 '(If you uploaded it with --no-squash, then branch dependencies '
2788 'are not supported, and you should reupload with --squash.)'
2789 % upstream_branch_name,
2790 change_desc)
2791 return parent
2792
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002793 def _AddChangeIdToCommitMessage(self, options, args):
2794 """Re-commits using the current message, assumes the commit hook is in
2795 place.
2796 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002797 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002798 git_command = ['commit', '--amend', '-m', log_desc]
2799 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002800 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002801 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002802 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002803 return new_log_desc
2804 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002805 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002806
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002807 def SetCQState(self, new_state):
2808 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002809 vote_map = {
2810 _CQState.NONE: 0,
2811 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002812 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002813 }
Aaron Gablefc62f762017-07-17 11:12:07 -07002814 labels = {'Commit-Queue': vote_map[new_state]}
2815 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002816 gerrit_util.SetReview(
2817 self._GetGerritHost(), self._GerritChangeIdentifier(),
2818 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002819
tandriie113dfd2016-10-11 10:20:12 -07002820 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002821 try:
2822 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002823 except GerritChangeNotExists:
2824 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002825
2826 if data['status'] in ('ABANDONED', 'MERGED'):
2827 return 'CL %s is closed' % self.GetIssue()
2828
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002829 def GetTryJobProperties(self, patchset=None):
2830 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002831 data = self._GetChangeDetail(['ALL_REVISIONS'])
2832 patchset = int(patchset or self.GetPatchset())
2833 assert patchset
2834 revision_data = None # Pylint wants it to be defined.
2835 for revision_data in data['revisions'].itervalues():
2836 if int(revision_data['_number']) == patchset:
2837 break
2838 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002839 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002840 (patchset, self.GetIssue()))
2841 return {
2842 'patch_issue': self.GetIssue(),
2843 'patch_set': patchset or self.GetPatchset(),
2844 'patch_project': data['project'],
2845 'patch_storage': 'gerrit',
2846 'patch_ref': revision_data['fetch']['http']['ref'],
2847 'patch_repository_url': revision_data['fetch']['http']['url'],
2848 'patch_gerrit_url': self.GetCodereviewServer(),
2849 }
tandriie113dfd2016-10-11 10:20:12 -07002850
tandriide281ae2016-10-12 06:02:30 -07002851 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002852 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002853
Edward Lemur707d70b2018-02-07 00:50:14 +01002854 def GetReviewers(self):
2855 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002856 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002857
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002858
2859_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002860 'gerrit': _GerritChangelistImpl,
2861}
2862
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002863
iannuccie53c9352016-08-17 14:40:40 -07002864def _add_codereview_issue_select_options(parser, extra=""):
2865 _add_codereview_select_options(parser)
2866
2867 text = ('Operate on this issue number instead of the current branch\'s '
2868 'implicit issue.')
2869 if extra:
2870 text += ' '+extra
2871 parser.add_option('-i', '--issue', type=int, help=text)
2872
2873
2874def _process_codereview_issue_select_options(parser, options):
2875 _process_codereview_select_options(parser, options)
2876 if options.issue is not None and not options.forced_codereview:
2877 parser.error('--issue must be specified with either --rietveld or --gerrit')
2878
2879
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002880def _add_codereview_select_options(parser):
2881 """Appends --gerrit and --rietveld options to force specific codereview."""
2882 parser.codereview_group = optparse.OptionGroup(
2883 parser, 'EXPERIMENTAL! Codereview override options')
2884 parser.add_option_group(parser.codereview_group)
2885 parser.codereview_group.add_option(
2886 '--gerrit', action='store_true',
2887 help='Force the use of Gerrit for codereview')
2888 parser.codereview_group.add_option(
2889 '--rietveld', action='store_true',
2890 help='Force the use of Rietveld for codereview')
2891
2892
2893def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00002894 if options.rietveld:
2895 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002896 options.forced_codereview = None
2897 if options.gerrit:
2898 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002899
2900
tandriif9aefb72016-07-01 09:06:51 -07002901def _get_bug_line_values(default_project, bugs):
2902 """Given default_project and comma separated list of bugs, yields bug line
2903 values.
2904
2905 Each bug can be either:
2906 * a number, which is combined with default_project
2907 * string, which is left as is.
2908
2909 This function may produce more than one line, because bugdroid expects one
2910 project per line.
2911
2912 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2913 ['v8:123', 'chromium:789']
2914 """
2915 default_bugs = []
2916 others = []
2917 for bug in bugs.split(','):
2918 bug = bug.strip()
2919 if bug:
2920 try:
2921 default_bugs.append(int(bug))
2922 except ValueError:
2923 others.append(bug)
2924
2925 if default_bugs:
2926 default_bugs = ','.join(map(str, default_bugs))
2927 if default_project:
2928 yield '%s:%s' % (default_project, default_bugs)
2929 else:
2930 yield default_bugs
2931 for other in sorted(others):
2932 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2933 yield other
2934
2935
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002936class ChangeDescription(object):
2937 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002938 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002939 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002940 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002941 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002942 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2943 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
2944 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
2945 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002946
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002947 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002948 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002949
agable@chromium.org42c20792013-09-12 17:34:49 +00002950 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002951 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002952 return '\n'.join(self._description_lines)
2953
2954 def set_description(self, desc):
2955 if isinstance(desc, basestring):
2956 lines = desc.splitlines()
2957 else:
2958 lines = [line.rstrip() for line in desc]
2959 while lines and not lines[0]:
2960 lines.pop(0)
2961 while lines and not lines[-1]:
2962 lines.pop(-1)
2963 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002964
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002965 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
2966 """Rewrites the R=/TBR= line(s) as a single line each.
2967
2968 Args:
2969 reviewers (list(str)) - list of additional emails to use for reviewers.
2970 tbrs (list(str)) - list of additional emails to use for TBRs.
2971 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
2972 the change that are missing OWNER coverage. If this is not None, you
2973 must also pass a value for `change`.
2974 change (Change) - The Change that should be used for OWNERS lookups.
2975 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002976 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002977 assert isinstance(tbrs, list), tbrs
2978
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002979 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07002980 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002981
2982 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002983 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002984
2985 reviewers = set(reviewers)
2986 tbrs = set(tbrs)
2987 LOOKUP = {
2988 'TBR': tbrs,
2989 'R': reviewers,
2990 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002991
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002992 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00002993 regexp = re.compile(self.R_LINE)
2994 matches = [regexp.match(line) for line in self._description_lines]
2995 new_desc = [l for i, l in enumerate(self._description_lines)
2996 if not matches[i]]
2997 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002998
agable@chromium.org42c20792013-09-12 17:34:49 +00002999 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003000
3001 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003002 for match in matches:
3003 if not match:
3004 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003005 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3006
3007 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003008 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003009 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003010 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003011 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003012 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003013 LOOKUP[add_owners_to].update(
3014 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003015
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003016 # If any folks ended up in both groups, remove them from tbrs.
3017 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003018
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003019 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3020 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003021
3022 # Put the new lines in the description where the old first R= line was.
3023 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3024 if 0 <= line_loc < len(self._description_lines):
3025 if new_tbr_line:
3026 self._description_lines.insert(line_loc, new_tbr_line)
3027 if new_r_line:
3028 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003029 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003030 if new_r_line:
3031 self.append_footer(new_r_line)
3032 if new_tbr_line:
3033 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003034
Aaron Gable3a16ed12017-03-23 10:51:55 -07003035 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003036 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003037 self.set_description([
3038 '# Enter a description of the change.',
3039 '# This will be displayed on the codereview site.',
3040 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003041 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003042 '--------------------',
3043 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003044
agable@chromium.org42c20792013-09-12 17:34:49 +00003045 regexp = re.compile(self.BUG_LINE)
3046 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003047 prefix = settings.GetBugPrefix()
3048 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003049 if git_footer:
3050 self.append_footer('Bug: %s' % ', '.join(values))
3051 else:
3052 for value in values:
3053 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003054
agable@chromium.org42c20792013-09-12 17:34:49 +00003055 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003056 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003057 if not content:
3058 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003059 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003060
Bruce Dawson2377b012018-01-11 16:46:49 -08003061 # Strip off comments and default inserted "Bug:" line.
3062 clean_lines = [line.rstrip() for line in lines if not
3063 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003064 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003065 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003066 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003067
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003068 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003069 """Adds a footer line to the description.
3070
3071 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3072 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3073 that Gerrit footers are always at the end.
3074 """
3075 parsed_footer_line = git_footers.parse_footer(line)
3076 if parsed_footer_line:
3077 # Line is a gerrit footer in the form: Footer-Key: any value.
3078 # Thus, must be appended observing Gerrit footer rules.
3079 self.set_description(
3080 git_footers.add_footer(self.description,
3081 key=parsed_footer_line[0],
3082 value=parsed_footer_line[1]))
3083 return
3084
3085 if not self._description_lines:
3086 self._description_lines.append(line)
3087 return
3088
3089 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3090 if gerrit_footers:
3091 # git_footers.split_footers ensures that there is an empty line before
3092 # actual (gerrit) footers, if any. We have to keep it that way.
3093 assert top_lines and top_lines[-1] == ''
3094 top_lines, separator = top_lines[:-1], top_lines[-1:]
3095 else:
3096 separator = [] # No need for separator if there are no gerrit_footers.
3097
3098 prev_line = top_lines[-1] if top_lines else ''
3099 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3100 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3101 top_lines.append('')
3102 top_lines.append(line)
3103 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003104
tandrii99a72f22016-08-17 14:33:24 -07003105 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003106 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003107 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003108 reviewers = [match.group(2).strip()
3109 for match in matches
3110 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003111 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003112
bradnelsond975b302016-10-23 12:20:23 -07003113 def get_cced(self):
3114 """Retrieves the list of reviewers."""
3115 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3116 cced = [match.group(2).strip() for match in matches if match]
3117 return cleanup_list(cced)
3118
Nodir Turakulov23b82142017-11-16 11:04:25 -08003119 def get_hash_tags(self):
3120 """Extracts and sanitizes a list of Gerrit hashtags."""
3121 subject = (self._description_lines or ('',))[0]
3122 subject = re.sub(
3123 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3124
3125 tags = []
3126 start = 0
3127 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3128 while True:
3129 m = bracket_exp.match(subject, start)
3130 if not m:
3131 break
3132 tags.append(self.sanitize_hash_tag(m.group(1)))
3133 start = m.end()
3134
3135 if not tags:
3136 # Try "Tag: " prefix.
3137 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3138 if m:
3139 tags.append(self.sanitize_hash_tag(m.group(1)))
3140 return tags
3141
3142 @classmethod
3143 def sanitize_hash_tag(cls, tag):
3144 """Returns a sanitized Gerrit hash tag.
3145
3146 A sanitized hashtag can be used as a git push refspec parameter value.
3147 """
3148 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3149
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003150 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3151 """Updates this commit description given the parent.
3152
3153 This is essentially what Gnumbd used to do.
3154 Consult https://goo.gl/WMmpDe for more details.
3155 """
3156 assert parent_msg # No, orphan branch creation isn't supported.
3157 assert parent_hash
3158 assert dest_ref
3159 parent_footer_map = git_footers.parse_footers(parent_msg)
3160 # This will also happily parse svn-position, which GnumbD is no longer
3161 # supporting. While we'd generate correct footers, the verifier plugin
3162 # installed in Gerrit will block such commit (ie git push below will fail).
3163 parent_position = git_footers.get_position(parent_footer_map)
3164
3165 # Cherry-picks may have last line obscuring their prior footers,
3166 # from git_footers perspective. This is also what Gnumbd did.
3167 cp_line = None
3168 if (self._description_lines and
3169 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3170 cp_line = self._description_lines.pop()
3171
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003172 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003173
3174 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3175 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003176 for i, line in enumerate(footer_lines):
3177 k, v = git_footers.parse_footer(line) or (None, None)
3178 if k and k.startswith('Cr-'):
3179 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003180
3181 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003182 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003183 if parent_position[0] == dest_ref:
3184 # Same branch as parent.
3185 number = int(parent_position[1]) + 1
3186 else:
3187 number = 1 # New branch, and extra lineage.
3188 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3189 int(parent_position[1])))
3190
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003191 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3192 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003193
3194 self._description_lines = top_lines
3195 if cp_line:
3196 self._description_lines.append(cp_line)
3197 if self._description_lines[-1] != '':
3198 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003199 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003200
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003201
Aaron Gablea1bab272017-04-11 16:38:18 -07003202def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003203 """Retrieves the reviewers that approved a CL from the issue properties with
3204 messages.
3205
3206 Note that the list may contain reviewers that are not committer, thus are not
3207 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003208
3209 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003210 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003211 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003212 return sorted(
3213 set(
3214 message['sender']
3215 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003216 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003217 )
3218 )
3219
3220
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003221def FindCodereviewSettingsFile(filename='codereview.settings'):
3222 """Finds the given file starting in the cwd and going up.
3223
3224 Only looks up to the top of the repository unless an
3225 'inherit-review-settings-ok' file exists in the root of the repository.
3226 """
3227 inherit_ok_file = 'inherit-review-settings-ok'
3228 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003229 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003230 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3231 root = '/'
3232 while True:
3233 if filename in os.listdir(cwd):
3234 if os.path.isfile(os.path.join(cwd, filename)):
3235 return open(os.path.join(cwd, filename))
3236 if cwd == root:
3237 break
3238 cwd = os.path.dirname(cwd)
3239
3240
3241def LoadCodereviewSettingsFromFile(fileobj):
3242 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003243 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003244
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003245 def SetProperty(name, setting, unset_error_ok=False):
3246 fullname = 'rietveld.' + name
3247 if setting in keyvals:
3248 RunGit(['config', fullname, keyvals[setting]])
3249 else:
3250 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3251
tandrii48df5812016-10-17 03:55:37 -07003252 if not keyvals.get('GERRIT_HOST', False):
3253 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003254 # Only server setting is required. Other settings can be absent.
3255 # In that case, we ignore errors raised during option deletion attempt.
3256 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3257 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3258 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003259 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003260 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3261 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003262 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3263 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003264
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003265 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003266 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003267
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003268 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003269 RunGit(['config', 'gerrit.squash-uploads',
3270 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003271
tandrii@chromium.org28253532016-04-14 13:46:56 +00003272 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003273 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003274 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3275
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003276 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003277 # should be of the form
3278 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3279 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003280 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3281 keyvals['ORIGIN_URL_CONFIG']])
3282
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003283
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003284def urlretrieve(source, destination):
3285 """urllib is broken for SSL connections via a proxy therefore we
3286 can't use urllib.urlretrieve()."""
3287 with open(destination, 'w') as f:
3288 f.write(urllib2.urlopen(source).read())
3289
3290
ukai@chromium.org712d6102013-11-27 00:52:58 +00003291def hasSheBang(fname):
3292 """Checks fname is a #! script."""
3293 with open(fname) as f:
3294 return f.read(2).startswith('#!')
3295
3296
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003297# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3298def DownloadHooks(*args, **kwargs):
3299 pass
3300
3301
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003302def DownloadGerritHook(force):
3303 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003304
3305 Args:
3306 force: True to update hooks. False to install hooks if not present.
3307 """
3308 if not settings.GetIsGerrit():
3309 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003310 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003311 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3312 if not os.access(dst, os.X_OK):
3313 if os.path.exists(dst):
3314 if not force:
3315 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003316 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003317 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003318 if not hasSheBang(dst):
3319 DieWithError('Not a script: %s\n'
3320 'You need to download from\n%s\n'
3321 'into .git/hooks/commit-msg and '
3322 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003323 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3324 except Exception:
3325 if os.path.exists(dst):
3326 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003327 DieWithError('\nFailed to download hooks.\n'
3328 'You need to download from\n%s\n'
3329 'into .git/hooks/commit-msg and '
3330 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003331
3332
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003333class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003334 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003335
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003336 _GOOGLESOURCE = 'googlesource.com'
3337
3338 def __init__(self):
3339 # Cached list of [host, identity, source], where source is either
3340 # .gitcookies or .netrc.
3341 self._all_hosts = None
3342
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003343 def ensure_configured_gitcookies(self):
3344 """Runs checks and suggests fixes to make git use .gitcookies from default
3345 path."""
3346 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3347 configured_path = RunGitSilent(
3348 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003349 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003350 if configured_path:
3351 self._ensure_default_gitcookies_path(configured_path, default)
3352 else:
3353 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003354
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003355 @staticmethod
3356 def _ensure_default_gitcookies_path(configured_path, default_path):
3357 assert configured_path
3358 if configured_path == default_path:
3359 print('git is already configured to use your .gitcookies from %s' %
3360 configured_path)
3361 return
3362
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003363 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003364 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3365 (configured_path, default_path))
3366
3367 if not os.path.exists(configured_path):
3368 print('However, your configured .gitcookies file is missing.')
3369 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3370 action='reconfigure')
3371 RunGit(['config', '--global', 'http.cookiefile', default_path])
3372 return
3373
3374 if os.path.exists(default_path):
3375 print('WARNING: default .gitcookies file already exists %s' %
3376 default_path)
3377 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3378 default_path)
3379
3380 confirm_or_exit('Move existing .gitcookies to default location?',
3381 action='move')
3382 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003383 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003384 print('Moved and reconfigured git to use .gitcookies from %s' %
3385 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003386
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003387 @staticmethod
3388 def _configure_gitcookies_path(default_path):
3389 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3390 if os.path.exists(netrc_path):
3391 print('You seem to be using outdated .netrc for git credentials: %s' %
3392 netrc_path)
3393 print('This tool will guide you through setting up recommended '
3394 '.gitcookies store for git credentials.\n'
3395 '\n'
3396 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3397 ' git config --global --unset http.cookiefile\n'
3398 ' mv %s %s.backup\n\n' % (default_path, default_path))
3399 confirm_or_exit(action='setup .gitcookies')
3400 RunGit(['config', '--global', 'http.cookiefile', default_path])
3401 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003402
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003403 def get_hosts_with_creds(self, include_netrc=False):
3404 if self._all_hosts is None:
3405 a = gerrit_util.CookiesAuthenticator()
3406 self._all_hosts = [
3407 (h, u, s)
3408 for h, u, s in itertools.chain(
3409 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3410 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3411 )
3412 if h.endswith(self._GOOGLESOURCE)
3413 ]
3414
3415 if include_netrc:
3416 return self._all_hosts
3417 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3418
3419 def print_current_creds(self, include_netrc=False):
3420 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3421 if not hosts:
3422 print('No Git/Gerrit credentials found')
3423 return
3424 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3425 header = [('Host', 'User', 'Which file'),
3426 ['=' * l for l in lengths]]
3427 for row in (header + hosts):
3428 print('\t'.join((('%%+%ds' % l) % s)
3429 for l, s in zip(lengths, row)))
3430
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003431 @staticmethod
3432 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003433 """Parses identity "git-<username>.domain" into <username> and domain."""
3434 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003435 # distinguishable from sub-domains. But we do know typical domains:
3436 if identity.endswith('.chromium.org'):
3437 domain = 'chromium.org'
3438 username = identity[:-len('.chromium.org')]
3439 else:
3440 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003441 if username.startswith('git-'):
3442 username = username[len('git-'):]
3443 return username, domain
3444
3445 def _get_usernames_of_domain(self, domain):
3446 """Returns list of usernames referenced by .gitcookies in a given domain."""
3447 identities_by_domain = {}
3448 for _, identity, _ in self.get_hosts_with_creds():
3449 username, domain = self._parse_identity(identity)
3450 identities_by_domain.setdefault(domain, []).append(username)
3451 return identities_by_domain.get(domain)
3452
3453 def _canonical_git_googlesource_host(self, host):
3454 """Normalizes Gerrit hosts (with '-review') to Git host."""
3455 assert host.endswith(self._GOOGLESOURCE)
3456 # Prefix doesn't include '.' at the end.
3457 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3458 if prefix.endswith('-review'):
3459 prefix = prefix[:-len('-review')]
3460 return prefix + '.' + self._GOOGLESOURCE
3461
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003462 def _canonical_gerrit_googlesource_host(self, host):
3463 git_host = self._canonical_git_googlesource_host(host)
3464 prefix = git_host.split('.', 1)[0]
3465 return prefix + '-review.' + self._GOOGLESOURCE
3466
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003467 def _get_counterpart_host(self, host):
3468 assert host.endswith(self._GOOGLESOURCE)
3469 git = self._canonical_git_googlesource_host(host)
3470 gerrit = self._canonical_gerrit_googlesource_host(git)
3471 return git if gerrit == host else gerrit
3472
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003473 def has_generic_host(self):
3474 """Returns whether generic .googlesource.com has been configured.
3475
3476 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3477 """
3478 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3479 if host == '.' + self._GOOGLESOURCE:
3480 return True
3481 return False
3482
3483 def _get_git_gerrit_identity_pairs(self):
3484 """Returns map from canonic host to pair of identities (Git, Gerrit).
3485
3486 One of identities might be None, meaning not configured.
3487 """
3488 host_to_identity_pairs = {}
3489 for host, identity, _ in self.get_hosts_with_creds():
3490 canonical = self._canonical_git_googlesource_host(host)
3491 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3492 idx = 0 if canonical == host else 1
3493 pair[idx] = identity
3494 return host_to_identity_pairs
3495
3496 def get_partially_configured_hosts(self):
3497 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003498 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3499 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3500 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003501
3502 def get_conflicting_hosts(self):
3503 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003504 host
3505 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003506 if None not in (i1, i2) and i1 != i2)
3507
3508 def get_duplicated_hosts(self):
3509 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3510 return set(host for host, count in counters.iteritems() if count > 1)
3511
3512 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3513 'chromium.googlesource.com': 'chromium.org',
3514 'chrome-internal.googlesource.com': 'google.com',
3515 }
3516
3517 def get_hosts_with_wrong_identities(self):
3518 """Finds hosts which **likely** reference wrong identities.
3519
3520 Note: skips hosts which have conflicting identities for Git and Gerrit.
3521 """
3522 hosts = set()
3523 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3524 pair = self._get_git_gerrit_identity_pairs().get(host)
3525 if pair and pair[0] == pair[1]:
3526 _, domain = self._parse_identity(pair[0])
3527 if domain != expected:
3528 hosts.add(host)
3529 return hosts
3530
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003531 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003532 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003533 hosts = sorted(hosts)
3534 assert hosts
3535 if extra_column_func is None:
3536 extras = [''] * len(hosts)
3537 else:
3538 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003539 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3540 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003541 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003542 lines.append(tmpl % he)
3543 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003544
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003545 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003546 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003547 yield ('.googlesource.com wildcard record detected',
3548 ['Chrome Infrastructure team recommends to list full host names '
3549 'explicitly.'],
3550 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003551
3552 dups = self.get_duplicated_hosts()
3553 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003554 yield ('The following hosts were defined twice',
3555 self._format_hosts(dups),
3556 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003557
3558 partial = self.get_partially_configured_hosts()
3559 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003560 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3561 'These hosts are missing',
3562 self._format_hosts(partial, lambda host: 'but %s defined' %
3563 self._get_counterpart_host(host)),
3564 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003565
3566 conflicting = self.get_conflicting_hosts()
3567 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003568 yield ('The following Git hosts have differing credentials from their '
3569 'Gerrit counterparts',
3570 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3571 tuple(self._get_git_gerrit_identity_pairs()[host])),
3572 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003573
3574 wrong = self.get_hosts_with_wrong_identities()
3575 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003576 yield ('These hosts likely use wrong identity',
3577 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3578 (self._get_git_gerrit_identity_pairs()[host][0],
3579 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3580 wrong)
3581
3582 def find_and_report_problems(self):
3583 """Returns True if there was at least one problem, else False."""
3584 found = False
3585 bad_hosts = set()
3586 for title, sublines, hosts in self._find_problems():
3587 if not found:
3588 found = True
3589 print('\n\n.gitcookies problem report:\n')
3590 bad_hosts.update(hosts or [])
3591 print(' %s%s' % (title , (':' if sublines else '')))
3592 if sublines:
3593 print()
3594 print(' %s' % '\n '.join(sublines))
3595 print()
3596
3597 if bad_hosts:
3598 assert found
3599 print(' You can manually remove corresponding lines in your %s file and '
3600 'visit the following URLs with correct account to generate '
3601 'correct credential lines:\n' %
3602 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3603 print(' %s' % '\n '.join(sorted(set(
3604 gerrit_util.CookiesAuthenticator().get_new_password_url(
3605 self._canonical_git_googlesource_host(host))
3606 for host in bad_hosts
3607 ))))
3608 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003609
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003610
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003611@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003612def CMDcreds_check(parser, args):
3613 """Checks credentials and suggests changes."""
3614 _, _ = parser.parse_args(args)
3615
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003616 # Code below checks .gitcookies. Abort if using something else.
3617 authn = gerrit_util.Authenticator.get()
3618 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3619 if isinstance(authn, gerrit_util.GceAuthenticator):
3620 DieWithError(
3621 'This command is not designed for GCE, are you on a bot?\n'
3622 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3623 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003624 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003625 'This command is not designed for bot environment. It checks '
3626 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003627
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003628 checker = _GitCookiesChecker()
3629 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003630
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003631 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003632 checker.print_current_creds(include_netrc=True)
3633
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003634 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003635 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003636 return 0
3637 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003638
3639
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003640@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003641def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003642 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003643 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3644 branch = ShortBranchName(branchref)
3645 _, args = parser.parse_args(args)
3646 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003647 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003648 return RunGit(['config', 'branch.%s.base-url' % branch],
3649 error_ok=False).strip()
3650 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003651 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003652 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3653 error_ok=False).strip()
3654
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003655def color_for_status(status):
3656 """Maps a Changelist status to color, for CMDstatus and other tools."""
3657 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003658 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003659 'waiting': Fore.BLUE,
3660 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003661 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003662 'lgtm': Fore.GREEN,
3663 'commit': Fore.MAGENTA,
3664 'closed': Fore.CYAN,
3665 'error': Fore.WHITE,
3666 }.get(status, Fore.WHITE)
3667
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003668
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003669def get_cl_statuses(changes, fine_grained, max_processes=None):
3670 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003671
3672 If fine_grained is true, this will fetch CL statuses from the server.
3673 Otherwise, simply indicate if there's a matching url for the given branches.
3674
3675 If max_processes is specified, it is used as the maximum number of processes
3676 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3677 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003678
3679 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003680 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003681 if not changes:
3682 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003683
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003684 if not fine_grained:
3685 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003686 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003687 for cl in changes:
3688 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003689 return
3690
3691 # First, sort out authentication issues.
3692 logging.debug('ensuring credentials exist')
3693 for cl in changes:
3694 cl.EnsureAuthenticated(force=False, refresh=True)
3695
3696 def fetch(cl):
3697 try:
3698 return (cl, cl.GetStatus())
3699 except:
3700 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003701 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003702 raise
3703
3704 threads_count = len(changes)
3705 if max_processes:
3706 threads_count = max(1, min(threads_count, max_processes))
3707 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3708
3709 pool = ThreadPool(threads_count)
3710 fetched_cls = set()
3711 try:
3712 it = pool.imap_unordered(fetch, changes).__iter__()
3713 while True:
3714 try:
3715 cl, status = it.next(timeout=5)
3716 except multiprocessing.TimeoutError:
3717 break
3718 fetched_cls.add(cl)
3719 yield cl, status
3720 finally:
3721 pool.close()
3722
3723 # Add any branches that failed to fetch.
3724 for cl in set(changes) - fetched_cls:
3725 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003726
rmistry@google.com2dd99862015-06-22 12:22:18 +00003727
3728def upload_branch_deps(cl, args):
3729 """Uploads CLs of local branches that are dependents of the current branch.
3730
3731 If the local branch dependency tree looks like:
3732 test1 -> test2.1 -> test3.1
3733 -> test3.2
3734 -> test2.2 -> test3.3
3735
3736 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3737 run on the dependent branches in this order:
3738 test2.1, test3.1, test3.2, test2.2, test3.3
3739
3740 Note: This function does not rebase your local dependent branches. Use it when
3741 you make a change to the parent branch that will not conflict with its
3742 dependent branches, and you would like their dependencies updated in
3743 Rietveld.
3744 """
3745 if git_common.is_dirty_git_tree('upload-branch-deps'):
3746 return 1
3747
3748 root_branch = cl.GetBranch()
3749 if root_branch is None:
3750 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3751 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003752 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003753 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3754 'patchset dependencies without an uploaded CL.')
3755
3756 branches = RunGit(['for-each-ref',
3757 '--format=%(refname:short) %(upstream:short)',
3758 'refs/heads'])
3759 if not branches:
3760 print('No local branches found.')
3761 return 0
3762
3763 # Create a dictionary of all local branches to the branches that are dependent
3764 # on it.
3765 tracked_to_dependents = collections.defaultdict(list)
3766 for b in branches.splitlines():
3767 tokens = b.split()
3768 if len(tokens) == 2:
3769 branch_name, tracked = tokens
3770 tracked_to_dependents[tracked].append(branch_name)
3771
vapiera7fbd5a2016-06-16 09:17:49 -07003772 print()
3773 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003774 dependents = []
3775 def traverse_dependents_preorder(branch, padding=''):
3776 dependents_to_process = tracked_to_dependents.get(branch, [])
3777 padding += ' '
3778 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003779 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003780 dependents.append(dependent)
3781 traverse_dependents_preorder(dependent, padding)
3782 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003783 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003784
3785 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003786 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003787 return 0
3788
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003789 confirm_or_exit('This command will checkout all dependent branches and run '
3790 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003791
rmistry@google.com2dd99862015-06-22 12:22:18 +00003792 # Record all dependents that failed to upload.
3793 failures = {}
3794 # Go through all dependents, checkout the branch and upload.
3795 try:
3796 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003797 print()
3798 print('--------------------------------------')
3799 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003800 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003801 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003802 try:
3803 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003804 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003805 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003806 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003807 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003808 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003809 finally:
3810 # Swap back to the original root branch.
3811 RunGit(['checkout', '-q', root_branch])
3812
vapiera7fbd5a2016-06-16 09:17:49 -07003813 print()
3814 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003815 for dependent_branch in dependents:
3816 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003817 print(' %s : %s' % (dependent_branch, upload_status))
3818 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003819
3820 return 0
3821
3822
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003823@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003824def CMDarchive(parser, args):
3825 """Archives and deletes branches associated with closed changelists."""
3826 parser.add_option(
3827 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003828 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003829 parser.add_option(
3830 '-f', '--force', action='store_true',
3831 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003832 parser.add_option(
3833 '-d', '--dry-run', action='store_true',
3834 help='Skip the branch tagging and removal steps.')
3835 parser.add_option(
3836 '-t', '--notags', action='store_true',
3837 help='Do not tag archived branches. '
3838 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003839
3840 auth.add_auth_options(parser)
3841 options, args = parser.parse_args(args)
3842 if args:
3843 parser.error('Unsupported args: %s' % ' '.join(args))
3844 auth_config = auth.extract_auth_config_from_options(options)
3845
3846 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3847 if not branches:
3848 return 0
3849
vapiera7fbd5a2016-06-16 09:17:49 -07003850 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003851 changes = [Changelist(branchref=b, auth_config=auth_config)
3852 for b in branches.splitlines()]
3853 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3854 statuses = get_cl_statuses(changes,
3855 fine_grained=True,
3856 max_processes=options.maxjobs)
3857 proposal = [(cl.GetBranch(),
3858 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3859 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003860 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003861 proposal.sort()
3862
3863 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003864 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003865 return 0
3866
3867 current_branch = GetCurrentBranch()
3868
vapiera7fbd5a2016-06-16 09:17:49 -07003869 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003870 if options.notags:
3871 for next_item in proposal:
3872 print(' ' + next_item[0])
3873 else:
3874 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3875 for next_item in proposal:
3876 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003877
kmarshall9249e012016-08-23 12:02:16 -07003878 # Quit now on precondition failure or if instructed by the user, either
3879 # via an interactive prompt or by command line flags.
3880 if options.dry_run:
3881 print('\nNo changes were made (dry run).\n')
3882 return 0
3883 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003884 print('You are currently on a branch \'%s\' which is associated with a '
3885 'closed codereview issue, so archive cannot proceed. Please '
3886 'checkout another branch and run this command again.' %
3887 current_branch)
3888 return 1
kmarshall9249e012016-08-23 12:02:16 -07003889 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003890 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3891 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003892 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003893 return 1
3894
3895 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003896 if not options.notags:
3897 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003898 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003899
vapiera7fbd5a2016-06-16 09:17:49 -07003900 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003901
3902 return 0
3903
3904
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003905@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003906def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003907 """Show status of changelists.
3908
3909 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003910 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003911 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003912 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003913 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003914 - Magenta in the commit queue
3915 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003916 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003917
3918 Also see 'git cl comments'.
3919 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003920 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003921 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003922 parser.add_option('-f', '--fast', action='store_true',
3923 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003924 parser.add_option(
3925 '-j', '--maxjobs', action='store', type=int,
3926 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003927
3928 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003929 _add_codereview_issue_select_options(
3930 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003931 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003932 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003933 if args:
3934 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003935 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003936
iannuccie53c9352016-08-17 14:40:40 -07003937 if options.issue is not None and not options.field:
3938 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003939
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003940 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003941 cl = Changelist(auth_config=auth_config, issue=options.issue,
3942 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003943 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003944 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003945 elif options.field == 'id':
3946 issueid = cl.GetIssue()
3947 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003948 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003949 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003950 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003951 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003952 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003953 elif options.field == 'status':
3954 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003955 elif options.field == 'url':
3956 url = cl.GetIssueURL()
3957 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003958 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003959 return 0
3960
3961 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3962 if not branches:
3963 print('No local branch found.')
3964 return 0
3965
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003966 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003967 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003968 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003969 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003970 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003971 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003972 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003973
Daniel McArdlea23bf592019-02-12 00:25:12 +00003974 current_branch = GetCurrentBranch()
3975
3976 def FormatBranchName(branch, colorize=False):
3977 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3978 an asterisk when it is the current branch."""
3979
3980 asterisk = ""
3981 color = Fore.RESET
3982 if branch == current_branch:
3983 asterisk = "* "
3984 color = Fore.GREEN
3985 branch_name = ShortBranchName(branch)
3986
3987 if colorize:
3988 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00003989 return asterisk + branch_name
3990
3991 def LeftPadColorfulString(string_plain, string_colorful, alignment):
3992 """Pad string_colorful with spaces until it hits alignment. Use string_plain
3993 to compute required number of spaces."""
3994
3995 padding_amount = alignment - len(string_plain)
3996 padding = ' ' * padding_amount
3997 return padding + string_colorful
Daniel McArdlea23bf592019-02-12 00:25:12 +00003998
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003999 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004000
4001 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004002 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4003 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004004 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004005 c, status = output.next()
4006 branch_statuses[c.GetBranch()] = status
4007 status = branch_statuses.pop(branch)
4008 url = cl.GetIssueURL()
4009 if url and (not status or status == 'error'):
4010 # The issue probably doesn't exist anymore.
4011 url += ' (broken)'
4012
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004013 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004014 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004015 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004016 color = ''
4017 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004018 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004019
4020 branch_display_plain = FormatBranchName(branch)
4021 branch_display_color = FormatBranchName(branch, colorize=True)
4022 padded_branch_name = LeftPadColorfulString(branch_display_plain,
4023 branch_display_color, alignment)
4024
4025 print(' %s : %s%s %s%s' % (
4026 padded_branch_name, color, url, status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004027
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004028
vapiera7fbd5a2016-06-16 09:17:49 -07004029 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004030 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004031 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004032 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004033 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004034 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004035 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004036 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004037 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004038 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004039 print('Issue description:')
4040 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004041 return 0
4042
4043
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004044def colorize_CMDstatus_doc():
4045 """To be called once in main() to add colors to git cl status help."""
4046 colors = [i for i in dir(Fore) if i[0].isupper()]
4047
4048 def colorize_line(line):
4049 for color in colors:
4050 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004051 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004052 indent = len(line) - len(line.lstrip(' ')) + 1
4053 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4054 return line
4055
4056 lines = CMDstatus.__doc__.splitlines()
4057 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4058
4059
phajdan.jre328cf92016-08-22 04:12:17 -07004060def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004061 if path == '-':
4062 json.dump(contents, sys.stdout)
4063 else:
4064 with open(path, 'w') as f:
4065 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004066
4067
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004068@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004069@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004070def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004071 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004072
4073 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004074 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004075 parser.add_option('-r', '--reverse', action='store_true',
4076 help='Lookup the branch(es) for the specified issues. If '
4077 'no issues are specified, all branches with mapped '
4078 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004079 parser.add_option('--json',
4080 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004081 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004082 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004083 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004084
dnj@chromium.org406c4402015-03-03 17:22:28 +00004085 if options.reverse:
4086 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004087 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004088 # Reverse issue lookup.
4089 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004090
4091 git_config = {}
4092 for config in RunGit(['config', '--get-regexp',
4093 r'branch\..*issue']).splitlines():
4094 name, _space, val = config.partition(' ')
4095 git_config[name] = val
4096
dnj@chromium.org406c4402015-03-03 17:22:28 +00004097 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004098 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4099 config_key = _git_branch_config_key(ShortBranchName(branch),
4100 cls.IssueConfigKey())
4101 issue = git_config.get(config_key)
4102 if issue:
4103 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004104 if not args:
4105 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004106 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004107 for issue in args:
4108 if not issue:
4109 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004110 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004111 print('Branch for issue number %s: %s' % (
4112 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004113 if options.json:
4114 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004115 return 0
4116
4117 if len(args) > 0:
4118 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4119 if not issue.valid:
4120 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4121 'or no argument to list it.\n'
4122 'Maybe you want to run git cl status?')
4123 cl = Changelist(codereview=issue.codereview)
4124 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004125 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004126 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004127 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4128 if options.json:
4129 write_json(options.json, {
4130 'issue': cl.GetIssue(),
4131 'issue_url': cl.GetIssueURL(),
4132 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004133 return 0
4134
4135
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004136@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004137def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004138 """Shows or posts review comments for any changelist."""
4139 parser.add_option('-a', '--add-comment', dest='comment',
4140 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004141 parser.add_option('-p', '--publish', action='store_true',
4142 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004143 parser.add_option('-i', '--issue', dest='issue',
4144 help='review issue id (defaults to current issue). '
4145 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004146 parser.add_option('-m', '--machine-readable', dest='readable',
4147 action='store_false', default=True,
4148 help='output comments in a format compatible with '
4149 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004150 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004151 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004152 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004153 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004154 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004155 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004156 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004157
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004158 issue = None
4159 if options.issue:
4160 try:
4161 issue = int(options.issue)
4162 except ValueError:
4163 DieWithError('A review issue id is expected to be a number')
4164
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004165 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4166
4167 if not cl.IsGerrit():
4168 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004169
4170 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004171 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004172 return 0
4173
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004174 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4175 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004176 for comment in summary:
4177 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004178 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004179 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004180 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004181 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004182 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004183 elif comment.autogenerated:
4184 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004185 else:
4186 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004187 print('\n%s%s %s%s\n%s' % (
4188 color,
4189 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4190 comment.sender,
4191 Fore.RESET,
4192 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4193
smut@google.comc85ac942015-09-15 16:34:43 +00004194 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004195 def pre_serialize(c):
4196 dct = c.__dict__.copy()
4197 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4198 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004199 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004200 return 0
4201
4202
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004203@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004204@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004205def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004206 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004207 parser.add_option('-d', '--display', action='store_true',
4208 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004209 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004210 help='New description to set for this issue (- for stdin, '
4211 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004212 parser.add_option('-f', '--force', action='store_true',
4213 help='Delete any unpublished Gerrit edits for this issue '
4214 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004215
4216 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004217 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004218 options, args = parser.parse_args(args)
4219 _process_codereview_select_options(parser, options)
4220
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004221 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004222 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004223 target_issue_arg = ParseIssueNumberArgument(args[0],
4224 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004225 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004226 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004227
martiniss6eda05f2016-06-30 10:18:35 -07004228 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004229 'auth_config': auth.extract_auth_config_from_options(options),
4230 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004231 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004232 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004233 if target_issue_arg:
4234 kwargs['issue'] = target_issue_arg.issue
4235 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004236 if target_issue_arg.codereview and not options.forced_codereview:
4237 detected_codereview_from_url = True
4238 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004239
4240 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004241 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004242 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004243 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004244
4245 if detected_codereview_from_url:
4246 logging.info('canonical issue/change URL: %s (type: %s)\n',
4247 cl.GetIssueURL(), target_issue_arg.codereview)
4248
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004249 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004250
smut@google.com34fb6b12015-07-13 20:03:26 +00004251 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004252 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004253 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004254
4255 if options.new_description:
4256 text = options.new_description
4257 if text == '-':
4258 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004259 elif text == '+':
4260 base_branch = cl.GetCommonAncestorWithUpstream()
4261 change = cl.GetChange(base_branch, None, local_description=True)
4262 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004263
4264 description.set_description(text)
4265 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004266 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004267
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004268 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004269 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004270 return 0
4271
4272
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004273@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004274def CMDlint(parser, args):
4275 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004276 parser.add_option('--filter', action='append', metavar='-x,+y',
4277 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004278 auth.add_auth_options(parser)
4279 options, args = parser.parse_args(args)
4280 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004281
4282 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004283 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004284 try:
4285 import cpplint
4286 import cpplint_chromium
4287 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004288 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004289 return 1
4290
4291 # Change the current working directory before calling lint so that it
4292 # shows the correct base.
4293 previous_cwd = os.getcwd()
4294 os.chdir(settings.GetRoot())
4295 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004296 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004297 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4298 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004299 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004300 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004301 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004302
4303 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004304 command = args + files
4305 if options.filter:
4306 command = ['--filter=' + ','.join(options.filter)] + command
4307 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004308
4309 white_regex = re.compile(settings.GetLintRegex())
4310 black_regex = re.compile(settings.GetLintIgnoreRegex())
4311 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4312 for filename in filenames:
4313 if white_regex.match(filename):
4314 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004315 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004316 else:
4317 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4318 extra_check_functions)
4319 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004320 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004321 finally:
4322 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004323 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004324 if cpplint._cpplint_state.error_count != 0:
4325 return 1
4326 return 0
4327
4328
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004329@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004330def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004331 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004332 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004333 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004334 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004335 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004336 parser.add_option('--all', action='store_true',
4337 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004338 parser.add_option('--parallel', action='store_true',
4339 help='Run all tests specified by input_api.RunTests in all '
4340 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004341 auth.add_auth_options(parser)
4342 options, args = parser.parse_args(args)
4343 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004344
sbc@chromium.org71437c02015-04-09 19:29:40 +00004345 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004346 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004347 return 1
4348
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004349 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004350 if args:
4351 base_branch = args[0]
4352 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004353 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004354 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004355
Aaron Gable8076c282017-11-29 14:39:41 -08004356 if options.all:
4357 base_change = cl.GetChange(base_branch, None)
4358 files = [('M', f) for f in base_change.AllFiles()]
4359 change = presubmit_support.GitChange(
4360 base_change.Name(),
4361 base_change.FullDescriptionText(),
4362 base_change.RepositoryRoot(),
4363 files,
4364 base_change.issue,
4365 base_change.patchset,
4366 base_change.author_email,
4367 base_change._upstream)
4368 else:
4369 change = cl.GetChange(base_branch, None)
4370
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004371 cl.RunHook(
4372 committing=not options.upload,
4373 may_prompt=False,
4374 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004375 change=change,
4376 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004377 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004378
4379
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004380def GenerateGerritChangeId(message):
4381 """Returns Ixxxxxx...xxx change id.
4382
4383 Works the same way as
4384 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4385 but can be called on demand on all platforms.
4386
4387 The basic idea is to generate git hash of a state of the tree, original commit
4388 message, author/committer info and timestamps.
4389 """
4390 lines = []
4391 tree_hash = RunGitSilent(['write-tree'])
4392 lines.append('tree %s' % tree_hash.strip())
4393 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4394 if code == 0:
4395 lines.append('parent %s' % parent.strip())
4396 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4397 lines.append('author %s' % author.strip())
4398 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4399 lines.append('committer %s' % committer.strip())
4400 lines.append('')
4401 # Note: Gerrit's commit-hook actually cleans message of some lines and
4402 # whitespace. This code is not doing this, but it clearly won't decrease
4403 # entropy.
4404 lines.append(message)
4405 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4406 stdin='\n'.join(lines))
4407 return 'I%s' % change_hash.strip()
4408
4409
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004410def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004411 """Computes the remote branch ref to use for the CL.
4412
4413 Args:
4414 remote (str): The git remote for the CL.
4415 remote_branch (str): The git remote branch for the CL.
4416 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004417 """
4418 if not (remote and remote_branch):
4419 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004420
wittman@chromium.org455dc922015-01-26 20:15:50 +00004421 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004422 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004423 # refs, which are then translated into the remote full symbolic refs
4424 # below.
4425 if '/' not in target_branch:
4426 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4427 else:
4428 prefix_replacements = (
4429 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4430 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4431 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4432 )
4433 match = None
4434 for regex, replacement in prefix_replacements:
4435 match = re.search(regex, target_branch)
4436 if match:
4437 remote_branch = target_branch.replace(match.group(0), replacement)
4438 break
4439 if not match:
4440 # This is a branch path but not one we recognize; use as-is.
4441 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004442 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4443 # Handle the refs that need to land in different refs.
4444 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004445
wittman@chromium.org455dc922015-01-26 20:15:50 +00004446 # Create the true path to the remote branch.
4447 # Does the following translation:
4448 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4449 # * refs/remotes/origin/master -> refs/heads/master
4450 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4451 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4452 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4453 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4454 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4455 'refs/heads/')
4456 elif remote_branch.startswith('refs/remotes/branch-heads'):
4457 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004458
wittman@chromium.org455dc922015-01-26 20:15:50 +00004459 return remote_branch
4460
4461
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004462def cleanup_list(l):
4463 """Fixes a list so that comma separated items are put as individual items.
4464
4465 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4466 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4467 """
4468 items = sum((i.split(',') for i in l), [])
4469 stripped_items = (i.strip() for i in items)
4470 return sorted(filter(None, stripped_items))
4471
4472
Aaron Gable4db38df2017-11-03 14:59:07 -07004473@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004474@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004475def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004476 """Uploads the current changelist to codereview.
4477
4478 Can skip dependency patchset uploads for a branch by running:
4479 git config branch.branch_name.skip-deps-uploads True
4480 To unset run:
4481 git config --unset branch.branch_name.skip-deps-uploads
4482 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004483
4484 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4485 a bug number, this bug number is automatically populated in the CL
4486 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004487
4488 If subject contains text in square brackets or has "<text>: " prefix, such
4489 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4490 [git-cl] add support for hashtags
4491 Foo bar: implement foo
4492 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004493 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004494 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4495 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004496 parser.add_option('--bypass-watchlists', action='store_true',
4497 dest='bypass_watchlists',
4498 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004499 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004500 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004501 parser.add_option('--message', '-m', dest='message',
4502 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004503 parser.add_option('-b', '--bug',
4504 help='pre-populate the bug number(s) for this issue. '
4505 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004506 parser.add_option('--message-file', dest='message_file',
4507 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004508 parser.add_option('--title', '-t', dest='title',
4509 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004510 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004511 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004512 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004513 parser.add_option('--tbrs',
4514 action='append', default=[],
4515 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004516 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004517 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004518 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004519 parser.add_option('--hashtag', dest='hashtags',
4520 action='append', default=[],
4521 help=('Gerrit hashtag for new CL; '
4522 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004523 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004524 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004525 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004526 help='tell the commit queue to commit this patchset; '
4527 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004528 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004529 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004530 metavar='TARGET',
4531 help='Apply CL to remote ref TARGET. ' +
4532 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004533 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004534 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004535 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004536 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004537 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004538 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004539 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4540 const='TBR', help='add a set of OWNERS to TBR')
4541 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4542 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004543 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4544 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004545 help='Send the patchset to do a CQ dry run right after '
4546 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004547 parser.add_option('--dependencies', action='store_true',
4548 help='Uploads CLs of all the local branches that depend on '
4549 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004550 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4551 help='Sends your change to the CQ after an approval. Only '
4552 'works on repos that have the Auto-Submit label '
4553 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004554 parser.add_option('--parallel', action='store_true',
4555 help='Run all tests specified by input_api.RunTests in all '
4556 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004557
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004558 parser.add_option('--no-autocc', action='store_true',
4559 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004560 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004561 help='Set the review private. This implies --no-autocc.')
4562
rmistry@google.com2dd99862015-06-22 12:22:18 +00004563 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004564 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004565 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004566 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004567 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004568 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004569
sbc@chromium.org71437c02015-04-09 19:29:40 +00004570 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004571 return 1
4572
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004573 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004574 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004575 options.cc = cleanup_list(options.cc)
4576
tandriib80458a2016-06-23 12:20:07 -07004577 if options.message_file:
4578 if options.message:
4579 parser.error('only one of --message and --message-file allowed.')
4580 options.message = gclient_utils.FileRead(options.message_file)
4581 options.message_file = None
4582
tandrii4d0545a2016-07-06 03:56:49 -07004583 if options.cq_dry_run and options.use_commit_queue:
4584 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4585
Aaron Gableedbc4132017-09-11 13:22:28 -07004586 if options.use_commit_queue:
4587 options.send_mail = True
4588
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004589 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4590 settings.GetIsGerrit()
4591
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004592 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004593 if not cl.IsGerrit():
4594 # Error out with instructions for repos not yet configured for Gerrit.
4595 print('=====================================')
4596 print('NOTICE: Rietveld is no longer supported. '
4597 'You can upload changes to Gerrit with')
4598 print(' git cl upload --gerrit')
4599 print('or set Gerrit to be your default code review tool with')
4600 print(' git config gerrit.host true')
4601 print('=====================================')
4602 return 1
4603
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004604 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004605
4606
Francois Dorayd42c6812017-05-30 15:10:20 -04004607@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004608@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004609def CMDsplit(parser, args):
4610 """Splits a branch into smaller branches and uploads CLs.
4611
4612 Creates a branch and uploads a CL for each group of files modified in the
4613 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004614 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004615 the shared OWNERS file.
4616 """
4617 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004618 help="A text file containing a CL description in which "
4619 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004620 parser.add_option("-c", "--comment", dest="comment_file",
4621 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004622 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4623 default=False,
4624 help="List the files and reviewers for each CL that would "
4625 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004626 parser.add_option("--cq-dry-run", action='store_true',
4627 help="If set, will do a cq dry run for each uploaded CL. "
4628 "Please be careful when doing this; more than ~10 CLs "
4629 "has the potential to overload our build "
4630 "infrastructure. Try to upload these not during high "
4631 "load times (usually 11-3 Mountain View time). Email "
4632 "infra-dev@chromium.org with any questions.")
Takuto Ikuta51eca592019-02-14 19:40:52 +00004633 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4634 default=True,
4635 help='Sends your change to the CQ after an approval. Only '
4636 'works on repos that have the Auto-Submit label '
4637 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004638 options, _ = parser.parse_args(args)
4639
4640 if not options.description_file:
4641 parser.error('No --description flag specified.')
4642
4643 def WrappedCMDupload(args):
4644 return CMDupload(OptionParser(), args)
4645
4646 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004647 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004648 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004649
4650
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004651@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004652@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004653def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004654 """DEPRECATED: Used to commit the current changelist via git-svn."""
4655 message = ('git-cl no longer supports committing to SVN repositories via '
4656 'git-svn. You probably want to use `git cl land` instead.')
4657 print(message)
4658 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004659
4660
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004661# Two special branches used by git cl land.
4662MERGE_BRANCH = 'git-cl-commit'
4663CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4664
4665
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004666@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004667@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004668def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004669 """Commits the current changelist via git.
4670
4671 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4672 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004673 """
4674 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4675 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004676 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004677 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004678 parser.add_option('--parallel', action='store_true',
4679 help='Run all tests specified by input_api.RunTests in all '
4680 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004681 auth.add_auth_options(parser)
4682 (options, args) = parser.parse_args(args)
4683 auth_config = auth.extract_auth_config_from_options(options)
4684
4685 cl = Changelist(auth_config=auth_config)
4686
Robert Iannucci2e73d432018-03-14 01:10:47 -07004687 if not cl.IsGerrit():
4688 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004689
Robert Iannucci2e73d432018-03-14 01:10:47 -07004690 if not cl.GetIssue():
4691 DieWithError('You must upload the change first to Gerrit.\n'
4692 ' If you would rather have `git cl land` upload '
4693 'automatically for you, see http://crbug.com/642759')
4694 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004695 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004696
4697
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004698@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004699@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004700def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004701 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004702 parser.add_option('-b', dest='newbranch',
4703 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004704 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004705 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004706 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004707 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004708 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004709 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004710 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004711 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004712 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004713 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004714
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004715
4716 group = optparse.OptionGroup(
4717 parser,
4718 'Options for continuing work on the current issue uploaded from a '
4719 'different clone (e.g. different machine). Must be used independently '
4720 'from the other options. No issue number should be specified, and the '
4721 'branch must have an issue number associated with it')
4722 group.add_option('--reapply', action='store_true', dest='reapply',
4723 help='Reset the branch and reapply the issue.\n'
4724 'CAUTION: This will undo any local changes in this '
4725 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004726
4727 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004728 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004729 parser.add_option_group(group)
4730
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004731 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004732 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004733 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004734 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004735 auth_config = auth.extract_auth_config_from_options(options)
4736
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004737 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004738 if options.newbranch:
4739 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004740 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004741 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004742
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004743 cl = Changelist(auth_config=auth_config,
4744 codereview=options.forced_codereview)
4745 if not cl.GetIssue():
4746 parser.error('current branch must have an associated issue')
4747
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004748 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004749 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004750 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004751
4752 RunGit(['reset', '--hard', upstream])
4753 if options.pull:
4754 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004755
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004756 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4757 options.directory)
4758
4759 if len(args) != 1 or not args[0]:
4760 parser.error('Must specify issue number or url')
4761
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004762 target_issue_arg = ParseIssueNumberArgument(args[0],
4763 options.forced_codereview)
4764 if not target_issue_arg.valid:
4765 parser.error('invalid codereview url or CL id')
4766
4767 cl_kwargs = {
4768 'auth_config': auth_config,
4769 'codereview_host': target_issue_arg.hostname,
4770 'codereview': options.forced_codereview,
4771 }
4772 detected_codereview_from_url = False
4773 if target_issue_arg.codereview and not options.forced_codereview:
4774 detected_codereview_from_url = True
4775 cl_kwargs['codereview'] = target_issue_arg.codereview
4776 cl_kwargs['issue'] = target_issue_arg.issue
4777
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004778 # We don't want uncommitted changes mixed up with the patch.
4779 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004780 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004781
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004782 if options.newbranch:
4783 if options.force:
4784 RunGit(['branch', '-D', options.newbranch],
4785 stderr=subprocess2.PIPE, error_ok=True)
4786 RunGit(['new-branch', options.newbranch])
4787
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004788 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004789
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004790 if cl.IsGerrit():
4791 if options.reject:
4792 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004793 if options.directory:
4794 parser.error('--directory is not supported with Gerrit codereview.')
4795
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004796 if detected_codereview_from_url:
4797 print('canonical issue/change URL: %s (type: %s)\n' %
4798 (cl.GetIssueURL(), target_issue_arg.codereview))
4799
4800 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07004801 options.nocommit, options.directory,
4802 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004803
4804
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004805def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004806 """Fetches the tree status and returns either 'open', 'closed',
4807 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004808 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004809 if url:
4810 status = urllib2.urlopen(url).read().lower()
4811 if status.find('closed') != -1 or status == '0':
4812 return 'closed'
4813 elif status.find('open') != -1 or status == '1':
4814 return 'open'
4815 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004816 return 'unset'
4817
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004818
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004819def GetTreeStatusReason():
4820 """Fetches the tree status from a json url and returns the message
4821 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004822 url = settings.GetTreeStatusUrl()
4823 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004824 connection = urllib2.urlopen(json_url)
4825 status = json.loads(connection.read())
4826 connection.close()
4827 return status['message']
4828
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004829
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004830@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004831def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004832 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004833 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004834 status = GetTreeStatus()
4835 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004836 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004837 return 2
4838
vapiera7fbd5a2016-06-16 09:17:49 -07004839 print('The tree is %s' % status)
4840 print()
4841 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004842 if status != 'open':
4843 return 1
4844 return 0
4845
4846
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004847@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004848def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004849 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004850 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004851 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004852 '-b', '--bot', action='append',
4853 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4854 'times to specify multiple builders. ex: '
4855 '"-b win_rel -b win_layout". See '
4856 'the try server waterfall for the builders name and the tests '
4857 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004858 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004859 '-B', '--bucket', default='',
4860 help=('Buildbucket bucket to send the try requests.'))
4861 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004862 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004863 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004864 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004865 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004866 help='Revision to use for the try job; default: the revision will '
4867 'be determined by the try recipe that builder runs, which usually '
4868 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004869 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004870 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004871 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004872 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004873 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004874 '--category', default='git_cl_try', help='Specify custom build category.')
4875 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004876 '--project',
4877 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004878 'in recipe to determine to which repository or directory to '
4879 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004880 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004881 '-p', '--property', dest='properties', action='append', default=[],
4882 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004883 'key2=value2 etc. The value will be treated as '
4884 'json if decodable, or as string otherwise. '
4885 'NOTE: using this may make your try job not usable for CQ, '
4886 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004887 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004888 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4889 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004890 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004891 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09004892 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004893 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09004894 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004895 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004896
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004897 if options.master and options.master.startswith('luci.'):
4898 parser.error(
4899 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00004900 # Make sure that all properties are prop=value pairs.
4901 bad_params = [x for x in options.properties if '=' not in x]
4902 if bad_params:
4903 parser.error('Got properties with missing "=": %s' % bad_params)
4904
maruel@chromium.org15192402012-09-06 12:38:29 +00004905 if args:
4906 parser.error('Unknown arguments: %s' % args)
4907
Koji Ishii31c14782018-01-08 17:17:33 +09004908 cl = Changelist(auth_config=auth_config, issue=options.issue,
4909 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00004910 if not cl.GetIssue():
4911 parser.error('Need to upload first')
4912
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004913 if cl.IsGerrit():
4914 # HACK: warm up Gerrit change detail cache to save on RPCs.
4915 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4916
tandriie113dfd2016-10-11 10:20:12 -07004917 error_message = cl.CannotTriggerTryJobReason()
4918 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004919 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004920
borenet6c0efe62016-10-19 08:13:29 -07004921 if options.bucket and options.master:
4922 parser.error('Only one of --bucket and --master may be used.')
4923
qyearsley1fdfcb62016-10-24 13:22:03 -07004924 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004925
qyearsleydd49f942016-10-28 11:57:22 -07004926 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4927 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004928 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004929 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004930 print('git cl try with no bots now defaults to CQ dry run.')
4931 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4932 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004933
borenet6c0efe62016-10-19 08:13:29 -07004934 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004935 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004936 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004937 'of bot requires an initial job from a parent (usually a builder). '
4938 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004939 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004940 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004941
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004942 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07004943 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004944 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07004945 except BuildbucketResponseException as ex:
4946 print('ERROR: %s' % ex)
4947 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004948 return 0
4949
4950
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004951@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004952def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004953 """Prints info about try jobs associated with current CL."""
4954 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004955 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004956 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004957 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004958 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004959 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004960 '--color', action='store_true', default=setup_color.IS_TTY,
4961 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004962 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004963 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4964 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004965 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07004966 '--json', help=('Path of JSON output file to write try job results to,'
4967 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004968 parser.add_option_group(group)
4969 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07004970 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004971 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07004972 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004973 if args:
4974 parser.error('Unrecognized args: %s' % ' '.join(args))
4975
4976 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07004977 cl = Changelist(
4978 issue=options.issue, codereview=options.forced_codereview,
4979 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004980 if not cl.GetIssue():
4981 parser.error('Need to upload first')
4982
tandrii221ab252016-10-06 08:12:04 -07004983 patchset = options.patchset
4984 if not patchset:
4985 patchset = cl.GetMostRecentPatchset()
4986 if not patchset:
4987 parser.error('Codereview doesn\'t know about issue %s. '
4988 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004989 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07004990 cl.GetIssue())
4991
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004992 try:
tandrii221ab252016-10-06 08:12:04 -07004993 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004994 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004995 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004996 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004997 if options.json:
4998 write_try_results_json(options.json, jobs)
4999 else:
5000 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005001 return 0
5002
5003
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005004@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005005@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005006def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005007 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005008 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005009 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005010 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005011
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005012 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005013 if args:
5014 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005015 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005016 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005017 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005018 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005019
5020 # Clear configured merge-base, if there is one.
5021 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005022 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005023 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005024 return 0
5025
5026
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005027@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005028def CMDweb(parser, args):
5029 """Opens the current CL in the web browser."""
5030 _, args = parser.parse_args(args)
5031 if args:
5032 parser.error('Unrecognized args: %s' % ' '.join(args))
5033
5034 issue_url = Changelist().GetIssueURL()
5035 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005036 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005037 return 1
5038
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005039 # Redirect I/O before invoking browser to hide its output. For example, this
5040 # allows to hide "Created new window in existing browser session." message
5041 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5042 saved_stdout = os.dup(1)
5043 os.close(1)
5044 os.open(os.devnull, os.O_RDWR)
5045 try:
5046 webbrowser.open(issue_url)
5047 finally:
5048 os.dup2(saved_stdout, 1)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005049 return 0
5050
5051
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005052@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005053def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005054 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005055 parser.add_option('-d', '--dry-run', action='store_true',
5056 help='trigger in dry run mode')
5057 parser.add_option('-c', '--clear', action='store_true',
5058 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005059 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005060 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005061 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005062 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005063 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005064 if args:
5065 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005066 if options.dry_run and options.clear:
5067 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5068
iannuccie53c9352016-08-17 14:40:40 -07005069 cl = Changelist(auth_config=auth_config, issue=options.issue,
5070 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005071 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005072 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005073 elif options.dry_run:
5074 state = _CQState.DRY_RUN
5075 else:
5076 state = _CQState.COMMIT
5077 if not cl.GetIssue():
5078 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005079 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005080 return 0
5081
5082
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005083@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005084def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005085 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005086 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005087 auth.add_auth_options(parser)
5088 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005089 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005090 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005091 if args:
5092 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005093 cl = Changelist(auth_config=auth_config, issue=options.issue,
5094 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005095 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005096 if not cl.GetIssue():
5097 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005098 cl.CloseIssue()
5099 return 0
5100
5101
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005102@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005103def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005104 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005105 parser.add_option(
5106 '--stat',
5107 action='store_true',
5108 dest='stat',
5109 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005110 auth.add_auth_options(parser)
5111 options, args = parser.parse_args(args)
5112 auth_config = auth.extract_auth_config_from_options(options)
5113 if args:
5114 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005115
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005116 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005117 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005118 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005119 if not issue:
5120 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005121
Aaron Gablea718c3e2017-08-28 17:47:28 -07005122 base = cl._GitGetBranchConfigValue('last-upload-hash')
5123 if not base:
5124 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5125 if not base:
5126 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5127 revision_info = detail['revisions'][detail['current_revision']]
5128 fetch_info = revision_info['fetch']['http']
5129 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5130 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005131
Aaron Gablea718c3e2017-08-28 17:47:28 -07005132 cmd = ['git', 'diff']
5133 if options.stat:
5134 cmd.append('--stat')
5135 cmd.append(base)
5136 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005137
5138 return 0
5139
5140
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005141@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005142def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005143 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005144 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005145 '--ignore-current',
5146 action='store_true',
5147 help='Ignore the CL\'s current reviewers and start from scratch.')
5148 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005149 '--ignore-self',
5150 action='store_true',
5151 help='Do not consider CL\'s author as an owners.')
5152 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005153 '--no-color',
5154 action='store_true',
5155 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005156 parser.add_option(
5157 '--batch',
5158 action='store_true',
5159 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005160 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005161 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005162 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005163
5164 author = RunGit(['config', 'user.email']).strip() or None
5165
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005166 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005167
5168 if args:
5169 if len(args) > 1:
5170 parser.error('Unknown args')
5171 base_branch = args[0]
5172 else:
5173 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005174 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005175
5176 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005177 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5178
5179 if options.batch:
5180 db = owners.Database(change.RepositoryRoot(), file, os.path)
5181 print('\n'.join(db.reviewers_for(affected_files, author)))
5182 return 0
5183
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005184 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005185 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005186 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005187 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005188 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005189 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005190 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005191 override_files=change.OriginalOwnersFiles(),
5192 ignore_author=options.ignore_self).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:]))