blob: 7fe6c3060b4791fc8f4a859f06f4af2eb247ad06 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000032import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000033import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000035import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000036import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000037import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000038import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000039
40try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080041 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042except ImportError:
43 pass
44
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000045from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000046from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000058import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000059import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000060import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000061import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import presubmit_support
63import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040064import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067import watchlists
68
tandrii7400cf02016-06-21 08:48:07 -070069__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000070
tandrii9d2c7a32016-06-22 03:42:45 -070071COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080072POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000074REFS_THAT_ALIAS_TO_OTHER_REFS = {
75 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
76 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
77}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078
thestig@chromium.org44202a22014-03-11 19:22:18 +000079# Valid extensions for files we want to lint.
80DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
81DEFAULT_LINT_IGNORE_REGEX = r"$^"
82
Aiden Bennerc08566e2018-10-03 17:52:42 +000083# File name for yapf style config files.
84YAPF_CONFIG_FILENAME = '.style.yapf'
85
borenet6c0efe62016-10-19 08:13:29 -070086# Buildbucket master name prefix.
87MASTER_PREFIX = 'master.'
88
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000089# Shortcut since it quickly becomes redundant.
90Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000091
maruel@chromium.orgddd59412011-11-30 14:20:38 +000092# Initialized in main()
93settings = None
94
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010095# Used by tests/git_cl_test.py to add extra logging.
96# Inside the weirdly failing test, add this:
97# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -070098# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010099_IS_BEING_TESTED = False
100
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000101
Christopher Lamf732cd52017-01-24 12:40:11 +1100102def DieWithError(message, change_desc=None):
103 if change_desc:
104 SaveDescriptionBackup(change_desc)
105
vapiera7fbd5a2016-06-16 09:17:49 -0700106 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107 sys.exit(1)
108
109
Christopher Lamf732cd52017-01-24 12:40:11 +1100110def SaveDescriptionBackup(change_desc):
111 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000112 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100113 backup_file = open(backup_path, 'w')
114 backup_file.write(change_desc.description)
115 backup_file.close()
116
117
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000118def GetNoGitPagerEnv():
119 env = os.environ.copy()
120 # 'cat' is a magical git string that disables pagers on all platforms.
121 env['GIT_PAGER'] = 'cat'
122 return env
123
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000124
bsep@chromium.org627d9002016-04-29 00:00:52 +0000125def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000126 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000127 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000128 except subprocess2.CalledProcessError as e:
129 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000130 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000132 'Command "%s" failed.\n%s' % (
133 ' '.join(args), error_message or e.stdout or ''))
134 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000135
136
137def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000138 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000139 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000140
141
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000142def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000143 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700144 if suppress_stderr:
145 stderr = subprocess2.VOID
146 else:
147 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000148 try:
tandrii5d48c322016-08-18 16:19:37 -0700149 (out, _), code = subprocess2.communicate(['git'] + args,
150 env=GetNoGitPagerEnv(),
151 stdout=subprocess2.PIPE,
152 stderr=stderr)
153 return code, out
154 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900155 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700156 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000157
158
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000159def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000160 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000161 return RunGitWithCode(args, suppress_stderr=True)[1]
162
163
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000164def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000165 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000166 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000167 return (version.startswith(prefix) and
168 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000169
170
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000171def BranchExists(branch):
172 """Return True if specified branch exists."""
173 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
174 suppress_stderr=True)
175 return not code
176
177
tandrii2a16b952016-10-19 07:09:44 -0700178def time_sleep(seconds):
179 # Use this so that it can be mocked in tests without interfering with python
180 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700181 return time.sleep(seconds)
182
183
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000184def time_time():
185 # Use this so that it can be mocked in tests without interfering with python
186 # system machinery.
187 return time.time()
188
189
maruel@chromium.org90541732011-04-01 17:54:18 +0000190def ask_for_data(prompt):
191 try:
192 return raw_input(prompt)
193 except KeyboardInterrupt:
194 # Hide the exception.
195 sys.exit(1)
196
197
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100198def confirm_or_exit(prefix='', action='confirm'):
199 """Asks user to press enter to continue or press Ctrl+C to abort."""
200 if not prefix or prefix.endswith('\n'):
201 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100202 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100203 mid = ' Press'
204 elif prefix.endswith(' '):
205 mid = 'press'
206 else:
207 mid = ' press'
208 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
209
210
211def ask_for_explicit_yes(prompt):
212 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
213 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
214 while True:
215 if 'yes'.startswith(result):
216 return True
217 if 'no'.startswith(result):
218 return False
219 result = ask_for_data('Please, type yes or no: ').lower()
220
221
tandrii5d48c322016-08-18 16:19:37 -0700222def _git_branch_config_key(branch, key):
223 """Helper method to return Git config key for a branch."""
224 assert branch, 'branch name is required to set git config for it'
225 return 'branch.%s.%s' % (branch, key)
226
227
228def _git_get_branch_config_value(key, default=None, value_type=str,
229 branch=False):
230 """Returns git config value of given or current branch if any.
231
232 Returns default in all other cases.
233 """
234 assert value_type in (int, str, bool)
235 if branch is False: # Distinguishing default arg value from None.
236 branch = GetCurrentBranch()
237
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000238 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700239 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000240
tandrii5d48c322016-08-18 16:19:37 -0700241 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700242 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700243 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700244 # git config also has --int, but apparently git config suffers from integer
245 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700246 args.append(_git_branch_config_key(branch, key))
247 code, out = RunGitWithCode(args)
248 if code == 0:
249 value = out.strip()
250 if value_type == int:
251 return int(value)
252 if value_type == bool:
253 return bool(value.lower() == 'true')
254 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255 return default
256
257
tandrii5d48c322016-08-18 16:19:37 -0700258def _git_set_branch_config_value(key, value, branch=None, **kwargs):
259 """Sets the value or unsets if it's None of a git branch config.
260
261 Valid, though not necessarily existing, branch must be provided,
262 otherwise currently checked out branch is used.
263 """
264 if not branch:
265 branch = GetCurrentBranch()
266 assert branch, 'a branch name OR currently checked out branch is required'
267 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700268 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700269 if value is None:
270 args.append('--unset')
271 elif isinstance(value, bool):
272 args.append('--bool')
273 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700274 else:
tandrii33a46ff2016-08-23 05:53:40 -0700275 # git config also has --int, but apparently git config suffers from integer
276 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700277 value = str(value)
278 args.append(_git_branch_config_key(branch, key))
279 if value is not None:
280 args.append(value)
281 RunGit(args, **kwargs)
282
283
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100284def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700285 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100286
287 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
288 """
289 # Git also stores timezone offset, but it only affects visual display,
290 # actual point in time is defined by this timestamp only.
291 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
292
293
294def _git_amend_head(message, committer_timestamp):
295 """Amends commit with new message and desired committer_timestamp.
296
297 Sets committer timezone to UTC.
298 """
299 env = os.environ.copy()
300 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
301 return RunGit(['commit', '--amend', '-m', message], env=env)
302
303
machenbach@chromium.org45453142015-09-15 08:45:22 +0000304def _get_properties_from_options(options):
305 properties = dict(x.split('=', 1) for x in options.properties)
306 for key, val in properties.iteritems():
307 try:
308 properties[key] = json.loads(val)
309 except ValueError:
310 pass # If a value couldn't be evaluated, treat it as a string.
311 return properties
312
313
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000314def _prefix_master(master):
315 """Convert user-specified master name to full master name.
316
317 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
318 name, while the developers always use shortened master name
319 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
320 function does the conversion for buildbucket migration.
321 """
borenet6c0efe62016-10-19 08:13:29 -0700322 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000323 return master
borenet6c0efe62016-10-19 08:13:29 -0700324 return '%s%s' % (MASTER_PREFIX, master)
325
326
327def _unprefix_master(bucket):
328 """Convert bucket name to shortened master name.
329
330 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
331 name, while the developers always use shortened master name
332 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
333 function does the conversion for buildbucket migration.
334 """
335 if bucket.startswith(MASTER_PREFIX):
336 return bucket[len(MASTER_PREFIX):]
337 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338
339
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000340def _buildbucket_retry(operation_name, http, *args, **kwargs):
341 """Retries requests to buildbucket service and returns parsed json content."""
342 try_count = 0
343 while True:
344 response, content = http.request(*args, **kwargs)
345 try:
346 content_json = json.loads(content)
347 except ValueError:
348 content_json = None
349
350 # Buildbucket could return an error even if status==200.
351 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000352 error = content_json.get('error')
353 if error.get('code') == 403:
354 raise BuildbucketResponseException(
355 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000356 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000357 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 raise BuildbucketResponseException(msg)
359
360 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700361 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 raise BuildbucketResponseException(
363 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700364 'Please file bugs at http://crbug.com, '
365 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366 content)
367 return content_json
368 if response.status < 500 or try_count >= 2:
369 raise httplib2.HttpLib2Error(content)
370
371 # status >= 500 means transient failures.
372 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700373 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374 try_count += 1
375 assert False, 'unreachable'
376
377
qyearsley1fdfcb62016-10-24 13:22:03 -0700378def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700379 """Returns a dict mapping bucket names to builders and tests,
380 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700381 """
qyearsleydd49f942016-10-28 11:57:22 -0700382 # If no bots are listed, we try to get a set of builders and tests based
383 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700384 if not options.bot:
385 change = changelist.GetChange(
386 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700387 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700388 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700389 change=change,
390 changed_files=change.LocalPaths(),
391 repository_root=settings.GetRoot(),
392 default_presubmit=None,
393 project=None,
394 verbose=options.verbose,
395 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700396 if masters is None:
397 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100398 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700399
qyearsley1fdfcb62016-10-24 13:22:03 -0700400 if options.bucket:
401 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700402 if options.master:
403 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700404
qyearsleydd49f942016-10-28 11:57:22 -0700405 # If bots are listed but no master or bucket, then we need to find out
406 # the corresponding master for each bot.
407 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
408 if error_message:
409 option_parser.error(
410 'Tryserver master cannot be found because: %s\n'
411 'Please manually specify the tryserver master, e.g. '
412 '"-m tryserver.chromium.linux".' % error_message)
413 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700414
415
qyearsley123a4682016-10-26 09:12:17 -0700416def _get_bucket_map_for_builders(builders):
417 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700418 map_url = 'https://builders-map.appspot.com/'
419 try:
qyearsley123a4682016-10-26 09:12:17 -0700420 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700421 except urllib2.URLError as e:
422 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
423 (map_url, e))
424 except ValueError as e:
425 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700426 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700427 return None, 'Failed to build master map.'
428
qyearsley123a4682016-10-26 09:12:17 -0700429 bucket_map = {}
430 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800431 bucket = builders_map.get(builder, {}).get('bucket')
432 if bucket:
433 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700434 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700435
436
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800437def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700438 """Sends a request to Buildbucket to trigger try jobs for a changelist.
439
440 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700441 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700442 changelist: Changelist that the try jobs are associated with.
443 buckets: A nested dict mapping bucket names to builders to tests.
444 options: Command-line options.
445 """
tandriide281ae2016-10-12 06:02:30 -0700446 assert changelist.GetIssue(), 'CL must be uploaded first'
447 codereview_url = changelist.GetCodereviewServer()
448 assert codereview_url, 'CL must be uploaded first'
449 patchset = patchset or changelist.GetMostRecentPatchset()
450 assert patchset, 'CL must be uploaded first'
451
452 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700453 # Cache the buildbucket credentials under the codereview host key, so that
454 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700455 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000456 http = authenticator.authorize(httplib2.Http())
457 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700458
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459 buildbucket_put_url = (
460 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000461 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000462 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700463 hostname=codereview_host,
464 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000465 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700466
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700467 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800468 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700469 if options.clobber:
470 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700471 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700472 if extra_properties:
473 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000474
475 batch_req_body = {'builds': []}
476 print_text = []
477 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700478 for bucket, builders_and_tests in sorted(buckets.iteritems()):
479 print_text.append('Bucket: %s' % bucket)
480 master = None
481 if bucket.startswith(MASTER_PREFIX):
482 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000483 for builder, tests in sorted(builders_and_tests.iteritems()):
484 print_text.append(' %s: %s' % (builder, tests))
485 parameters = {
486 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000487 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100488 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000489 'revision': options.revision,
490 }],
tandrii8c5a3532016-11-04 07:52:02 -0700491 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000492 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000493 if 'presubmit' in builder.lower():
494 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000495 if tests:
496 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700497
498 tags = [
499 'builder:%s' % builder,
500 'buildset:%s' % buildset,
501 'user_agent:git_cl_try',
502 ]
503 if master:
504 parameters['properties']['master'] = master
505 tags.append('master:%s' % master)
506
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000507 batch_req_body['builds'].append(
508 {
509 'bucket': bucket,
510 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700512 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000513 }
514 )
515
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000516 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700517 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000518 http,
519 buildbucket_put_url,
520 'PUT',
521 body=json.dumps(batch_req_body),
522 headers={'Content-Type': 'application/json'}
523 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000524 print_text.append('To see results here, run: git cl try-results')
525 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700526 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000527
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000528
tandrii221ab252016-10-06 08:12:04 -0700529def fetch_try_jobs(auth_config, changelist, buildbucket_host,
530 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700531 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000532
qyearsley53f48a12016-09-01 10:45:13 -0700533 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534 """
tandrii221ab252016-10-06 08:12:04 -0700535 assert buildbucket_host
536 assert changelist.GetIssue(), 'CL must be uploaded first'
537 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
538 patchset = patchset or changelist.GetMostRecentPatchset()
539 assert patchset, 'CL must be uploaded first'
540
541 codereview_url = changelist.GetCodereviewServer()
542 codereview_host = urlparse.urlparse(codereview_url).hostname
543 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 if authenticator.has_cached_credentials():
545 http = authenticator.authorize(httplib2.Http())
546 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700547 print('Warning: Some results might be missing because %s' %
548 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700549 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 http = httplib2.Http()
551
552 http.force_exception_to_status_code = True
553
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000554 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700555 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700557 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 params = {'tag': 'buildset:%s' % buildset}
559
560 builds = {}
561 while True:
562 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700563 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700565 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566 for build in content.get('builds', []):
567 builds[build['id']] = build
568 if 'next_cursor' in content:
569 params['start_cursor'] = content['next_cursor']
570 else:
571 break
572 return builds
573
574
qyearsleyeab3c042016-08-24 09:18:28 -0700575def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576 """Prints nicely result of fetch_try_jobs."""
577 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700578 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 return
580
581 # Make a copy, because we'll be modifying builds dictionary.
582 builds = builds.copy()
583 builder_names_cache = {}
584
585 def get_builder(b):
586 try:
587 return builder_names_cache[b['id']]
588 except KeyError:
589 try:
590 parameters = json.loads(b['parameters_json'])
591 name = parameters['builder_name']
592 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700593 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700594 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000595 name = None
596 builder_names_cache[b['id']] = name
597 return name
598
599 def get_bucket(b):
600 bucket = b['bucket']
601 if bucket.startswith('master.'):
602 return bucket[len('master.'):]
603 return bucket
604
605 if options.print_master:
606 name_fmt = '%%-%ds %%-%ds' % (
607 max(len(str(get_bucket(b))) for b in builds.itervalues()),
608 max(len(str(get_builder(b))) for b in builds.itervalues()))
609 def get_name(b):
610 return name_fmt % (get_bucket(b), get_builder(b))
611 else:
612 name_fmt = '%%-%ds' % (
613 max(len(str(get_builder(b))) for b in builds.itervalues()))
614 def get_name(b):
615 return name_fmt % get_builder(b)
616
617 def sort_key(b):
618 return b['status'], b.get('result'), get_name(b), b.get('url')
619
620 def pop(title, f, color=None, **kwargs):
621 """Pop matching builds from `builds` dict and print them."""
622
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000623 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000624 colorize = str
625 else:
626 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
627
628 result = []
629 for b in builds.values():
630 if all(b.get(k) == v for k, v in kwargs.iteritems()):
631 builds.pop(b['id'])
632 result.append(b)
633 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700634 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000635 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700636 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000637
638 total = len(builds)
639 pop(status='COMPLETED', result='SUCCESS',
640 title='Successes:', color=Fore.GREEN,
641 f=lambda b: (get_name(b), b.get('url')))
642 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
643 title='Infra Failures:', color=Fore.MAGENTA,
644 f=lambda b: (get_name(b), b.get('url')))
645 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
646 title='Failures:', color=Fore.RED,
647 f=lambda b: (get_name(b), b.get('url')))
648 pop(status='COMPLETED', result='CANCELED',
649 title='Canceled:', color=Fore.MAGENTA,
650 f=lambda b: (get_name(b),))
651 pop(status='COMPLETED', result='FAILURE',
652 failure_reason='INVALID_BUILD_DEFINITION',
653 title='Wrong master/builder name:', color=Fore.MAGENTA,
654 f=lambda b: (get_name(b),))
655 pop(status='COMPLETED', result='FAILURE',
656 title='Other failures:',
657 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
658 pop(status='COMPLETED',
659 title='Other finished:',
660 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
661 pop(status='STARTED',
662 title='Started:', color=Fore.YELLOW,
663 f=lambda b: (get_name(b), b.get('url')))
664 pop(status='SCHEDULED',
665 title='Scheduled:',
666 f=lambda b: (get_name(b), 'id=%s' % b['id']))
667 # The last section is just in case buildbucket API changes OR there is a bug.
668 pop(title='Other:',
669 f=lambda b: (get_name(b), 'id=%s' % b['id']))
670 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700671 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000672
673
Aiden Bennerc08566e2018-10-03 17:52:42 +0000674def _ComputeDiffLineRanges(files, upstream_commit):
675 """Gets the changed line ranges for each file since upstream_commit.
676
677 Parses a git diff on provided files and returns a dict that maps a file name
678 to an ordered list of range tuples in the form (start_line, count).
679 Ranges are in the same format as a git diff.
680 """
681 # If files is empty then diff_output will be a full diff.
682 if len(files) == 0:
683 return {}
684
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000685 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000686 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
687 diff_output = RunGit(diff_cmd)
688
689 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
690 # 2 capture groups
691 # 0 == fname of diff file
692 # 1 == 'diff_start,diff_count' or 'diff_start'
693 # will match each of
694 # diff --git a/foo.foo b/foo.py
695 # @@ -12,2 +14,3 @@
696 # @@ -12,2 +17 @@
697 # running re.findall on the above string with pattern will give
698 # [('foo.py', ''), ('', '14,3'), ('', '17')]
699
700 curr_file = None
701 line_diffs = {}
702 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
703 if match[0] != '':
704 # Will match the second filename in diff --git a/a.py b/b.py.
705 curr_file = match[0]
706 line_diffs[curr_file] = []
707 else:
708 # Matches +14,3
709 if ',' in match[1]:
710 diff_start, diff_count = match[1].split(',')
711 else:
712 # Single line changes are of the form +12 instead of +12,1.
713 diff_start = match[1]
714 diff_count = 1
715
716 diff_start = int(diff_start)
717 diff_count = int(diff_count)
718
719 # If diff_count == 0 this is a removal we can ignore.
720 line_diffs[curr_file].append((diff_start, diff_count))
721
722 return line_diffs
723
724
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000725def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000726 """Checks if a yapf file is in any parent directory of fpath until top_dir.
727
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000728 Recursively checks parent directories to find yapf file and if no yapf file
729 is found returns None. Uses yapf_config_cache as a cache for
730 previously found configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000731 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000732 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000733 # Return result if we've already computed it.
734 if fpath in yapf_config_cache:
735 return yapf_config_cache[fpath]
736
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000737 parent_dir = os.path.dirname(fpath)
738 if os.path.isfile(fpath):
739 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000740 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000741 # Otherwise fpath is a directory
742 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
743 if os.path.isfile(yapf_file):
744 ret = yapf_file
745 elif fpath == top_dir or parent_dir == fpath:
746 # If we're at the top level directory, or if we're at root
747 # there is no provided style.
748 ret = None
749 else:
750 # Otherwise recurse on the current directory.
751 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000752 yapf_config_cache[fpath] = ret
753 return ret
754
755
qyearsley53f48a12016-09-01 10:45:13 -0700756def write_try_results_json(output_file, builds):
757 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
758
759 The input |builds| dict is assumed to be generated by Buildbucket.
760 Buildbucket documentation: http://goo.gl/G0s101
761 """
762
763 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800764 """Extracts some of the information from one build dict."""
765 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700766 return {
767 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700768 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800769 'builder_name': parameters.get('builder_name'),
770 'created_ts': build.get('created_ts'),
771 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700772 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800773 'result': build.get('result'),
774 'status': build.get('status'),
775 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700776 'url': build.get('url'),
777 }
778
779 converted = []
780 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000781 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700782 write_json(output_file, converted)
783
784
Aaron Gable13101a62018-02-09 13:20:41 -0800785def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000786 """Prints statistics about the change to the user."""
787 # --no-ext-diff is broken in some versions of Git, so try to work around
788 # this by overriding the environment (but there is still a problem if the
789 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000790 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000791 if 'GIT_EXTERNAL_DIFF' in env:
792 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000793
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000794 try:
795 stdout = sys.stdout.fileno()
796 except AttributeError:
797 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000798 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800799 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000800 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000801
802
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000803class BuildbucketResponseException(Exception):
804 pass
805
806
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807class Settings(object):
808 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000810 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 self.tree_status_url = None
812 self.viewvc_url = None
813 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000814 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000815 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000816 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000817 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818
819 def LazyUpdateIfNeeded(self):
820 """Updates the settings from a codereview.settings file, if available."""
821 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000822 # The only value that actually changes the behavior is
823 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000824 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000825 error_ok=True
826 ).strip().lower()
827
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000829 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000830 LoadCodereviewSettingsFromFile(cr_settings_file)
831 self.updated = True
832
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000833 @staticmethod
834 def GetRelativeRoot():
835 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000838 if self.root is None:
839 self.root = os.path.abspath(self.GetRelativeRoot())
840 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 def GetTreeStatusUrl(self, error_ok=False):
843 if not self.tree_status_url:
844 error_message = ('You must configure your tree status URL by running '
845 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000846 self.tree_status_url = self._GetConfig(
847 'rietveld.tree-status-url', error_ok=error_ok,
848 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 return self.tree_status_url
850
851 def GetViewVCUrl(self):
852 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000853 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 return self.viewvc_url
855
rmistry@google.com90752582014-01-14 21:04:50 +0000856 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000857 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000858
rmistry@google.com5626a922015-02-26 14:03:30 +0000859 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000860 run_post_upload_hook = self._GetConfig(
861 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000862 return run_post_upload_hook == "True"
863
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000864 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000865 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000866
ukai@chromium.orge8077812012-02-03 03:41:46 +0000867 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700868 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000869 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700870 self.is_gerrit = (
871 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000872 return self.is_gerrit
873
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000874 def GetSquashGerritUploads(self):
875 """Return true if uploads to Gerrit should be squashed by default."""
876 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700877 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
878 if self.squash_gerrit_uploads is None:
879 # Default is squash now (http://crbug.com/611892#c23).
880 self.squash_gerrit_uploads = not (
881 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
882 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000883 return self.squash_gerrit_uploads
884
tandriia60502f2016-06-20 02:01:53 -0700885 def GetSquashGerritUploadsOverride(self):
886 """Return True or False if codereview.settings should be overridden.
887
888 Returns None if no override has been defined.
889 """
890 # See also http://crbug.com/611892#c23
891 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
892 error_ok=True).strip()
893 if result == 'true':
894 return True
895 if result == 'false':
896 return False
897 return None
898
tandrii@chromium.org28253532016-04-14 13:46:56 +0000899 def GetGerritSkipEnsureAuthenticated(self):
900 """Return True if EnsureAuthenticated should not be done for Gerrit
901 uploads."""
902 if self.gerrit_skip_ensure_authenticated is None:
903 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000904 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000905 error_ok=True).strip() == 'true')
906 return self.gerrit_skip_ensure_authenticated
907
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000908 def GetGitEditor(self):
909 """Return the editor specified in the git config, or None if none is."""
910 if self.git_editor is None:
911 self.git_editor = self._GetConfig('core.editor', error_ok=True)
912 return self.git_editor or None
913
thestig@chromium.org44202a22014-03-11 19:22:18 +0000914 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000915 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000916 DEFAULT_LINT_REGEX)
917
918 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000919 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000920 DEFAULT_LINT_IGNORE_REGEX)
921
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922 def _GetConfig(self, param, **kwargs):
923 self.LazyUpdateIfNeeded()
924 return RunGit(['config', param], **kwargs).strip()
925
926
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100927@contextlib.contextmanager
928def _get_gerrit_project_config_file(remote_url):
929 """Context manager to fetch and store Gerrit's project.config from
930 refs/meta/config branch and store it in temp file.
931
932 Provides a temporary filename or None if there was error.
933 """
934 error, _ = RunGitWithCode([
935 'fetch', remote_url,
936 '+refs/meta/config:refs/git_cl/meta/config'])
937 if error:
938 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700939 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100940 (remote_url, error))
941 yield None
942 return
943
944 error, project_config_data = RunGitWithCode(
945 ['show', 'refs/git_cl/meta/config:project.config'])
946 if error:
947 print('WARNING: project.config file not found')
948 yield None
949 return
950
951 with gclient_utils.temporary_directory() as tempdir:
952 project_config_file = os.path.join(tempdir, 'project.config')
953 gclient_utils.FileWrite(project_config_file, project_config_data)
954 yield project_config_file
955
956
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957def ShortBranchName(branch):
958 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000959 return branch.replace('refs/heads/', '', 1)
960
961
962def GetCurrentBranchRef():
963 """Returns branch ref (e.g., refs/heads/master) or None."""
964 return RunGit(['symbolic-ref', 'HEAD'],
965 stderr=subprocess2.VOID, error_ok=True).strip() or None
966
967
968def GetCurrentBranch():
969 """Returns current branch or None.
970
971 For refs/heads/* branches, returns just last part. For others, full ref.
972 """
973 branchref = GetCurrentBranchRef()
974 if branchref:
975 return ShortBranchName(branchref)
976 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977
978
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000979class _CQState(object):
980 """Enum for states of CL with respect to Commit Queue."""
981 NONE = 'none'
982 DRY_RUN = 'dry_run'
983 COMMIT = 'commit'
984
985 ALL_STATES = [NONE, DRY_RUN, COMMIT]
986
987
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000988class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200989 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000990 self.issue = issue
991 self.patchset = patchset
992 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +0000993 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200994 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000995
996 @property
997 def valid(self):
998 return self.issue is not None
999
1000
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001001def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001002 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1003 fail_result = _ParsedIssueNumberArgument()
1004
1005 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001006 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001007 if not arg.startswith('http'):
1008 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001009
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001010 url = gclient_utils.UpgradeToHttps(arg)
1011 try:
1012 parsed_url = urlparse.urlparse(url)
1013 except ValueError:
1014 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001015
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001016 if codereview is not None:
1017 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1018 return parsed or fail_result
1019
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001020 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001021
1022
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001023def _create_description_from_log(args):
1024 """Pulls out the commit log to use as a base for the CL description."""
1025 log_args = []
1026 if len(args) == 1 and not args[0].endswith('.'):
1027 log_args = [args[0] + '..']
1028 elif len(args) == 1 and args[0].endswith('...'):
1029 log_args = [args[0][:-1]]
1030 elif len(args) == 2:
1031 log_args = [args[0] + '..' + args[1]]
1032 else:
1033 log_args = args[:] # Hope for the best!
1034 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1035
1036
Aaron Gablea45ee112016-11-22 15:14:38 -08001037class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001038 def __init__(self, issue, url):
1039 self.issue = issue
1040 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001041 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001042
1043 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001044 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001045 self.issue, self.url)
1046
1047
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001048_CommentSummary = collections.namedtuple(
1049 '_CommentSummary', ['date', 'message', 'sender',
1050 # TODO(tandrii): these two aren't known in Gerrit.
1051 'approval', 'disapproval'])
1052
1053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001055 """Changelist works with one changelist in local branch.
1056
1057 Supports two codereview backends: Rietveld or Gerrit, selected at object
1058 creation.
1059
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001060 Notes:
1061 * Not safe for concurrent multi-{thread,process} use.
1062 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001063 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001064 """
1065
1066 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1067 """Create a new ChangeList instance.
1068
1069 If issue is given, the codereview must be given too.
1070
1071 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1072 Otherwise, it's decided based on current configuration of the local branch,
1073 with default being 'rietveld' for backwards compatibility.
1074 See _load_codereview_impl for more details.
1075
1076 **kwargs will be passed directly to codereview implementation.
1077 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001079 global settings
1080 if not settings:
1081 # Happens when git_cl.py is used as a utility library.
1082 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001083
1084 if issue:
1085 assert codereview, 'codereview must be known, if issue is known'
1086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087 self.branchref = branchref
1088 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001089 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 self.branch = ShortBranchName(self.branchref)
1091 else:
1092 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001094 self.lookedup_issue = False
1095 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001096 self.has_description = False
1097 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001098 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001100 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001101 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001102 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001103 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001104
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001105 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001106 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001107 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001108 assert self._codereview_impl
1109 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001110
1111 def _load_codereview_impl(self, codereview=None, **kwargs):
1112 if codereview:
Joe Masond87b0962018-12-03 21:04:46 +00001113 assert codereview in _CODEREVIEW_IMPLEMENTATIONS, (
1114 'codereview {} not in {}'.format(codereview,
1115 _CODEREVIEW_IMPLEMENTATIONS))
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001116 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1117 self._codereview = codereview
1118 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001119 return
1120
1121 # Automatic selection based on issue number set for a current branch.
1122 # Rietveld takes precedence over Gerrit.
1123 assert not self.issue
1124 # Whether we find issue or not, we are doing the lookup.
1125 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001126 if self.GetBranch():
1127 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1128 issue = _git_get_branch_config_value(
1129 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1130 if issue:
1131 self._codereview = codereview
1132 self._codereview_impl = cls(self, **kwargs)
1133 self.issue = int(issue)
1134 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001135
Bryce Thomascfc97122018-12-13 20:21:47 +00001136 # No issue is set for this branch, so default to gerrit.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001137 return self._load_codereview_impl(
Bryce Thomascfc97122018-12-13 20:21:47 +00001138 codereview='gerrit',
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001139 **kwargs)
1140
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001141 def IsGerrit(self):
1142 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001143
1144 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001145 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001146
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001147 The return value is a string suitable for passing to git cl with the --cc
1148 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001149 """
1150 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001151 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001152 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1154 return self.cc
1155
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001156 def GetCCListWithoutDefault(self):
1157 """Return the users cc'd on this CL excluding default ones."""
1158 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001159 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001160 return self.cc
1161
Daniel Cheng7227d212017-11-17 08:12:37 -08001162 def ExtendCC(self, more_cc):
1163 """Extends the list of users to cc on this CL based on the changed files."""
1164 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165
1166 def GetBranch(self):
1167 """Returns the short branch name, e.g. 'master'."""
1168 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001170 if not branchref:
1171 return None
1172 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 self.branch = ShortBranchName(self.branchref)
1174 return self.branch
1175
1176 def GetBranchRef(self):
1177 """Returns the full branch name, e.g. 'refs/heads/master'."""
1178 self.GetBranch() # Poke the lazy loader.
1179 return self.branchref
1180
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001181 def ClearBranch(self):
1182 """Clears cached branch data of this object."""
1183 self.branch = self.branchref = None
1184
tandrii5d48c322016-08-18 16:19:37 -07001185 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1186 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1187 kwargs['branch'] = self.GetBranch()
1188 return _git_get_branch_config_value(key, default, **kwargs)
1189
1190 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1191 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1192 assert self.GetBranch(), (
1193 'this CL must have an associated branch to %sset %s%s' %
1194 ('un' if value is None else '',
1195 key,
1196 '' if value is None else ' to %r' % value))
1197 kwargs['branch'] = self.GetBranch()
1198 return _git_set_branch_config_value(key, value, **kwargs)
1199
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001200 @staticmethod
1201 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001202 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203 e.g. 'origin', 'refs/heads/master'
1204 """
1205 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001206 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1207
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001209 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001211 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1212 error_ok=True).strip()
1213 if upstream_branch:
1214 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001216 # Else, try to guess the origin remote.
1217 remote_branches = RunGit(['branch', '-r']).split()
1218 if 'origin/master' in remote_branches:
1219 # Fall back on origin/master if it exits.
1220 remote = 'origin'
1221 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001223 DieWithError(
1224 'Unable to determine default branch to diff against.\n'
1225 'Either pass complete "git diff"-style arguments, like\n'
1226 ' git cl upload origin/master\n'
1227 'or verify this branch is set up to track another \n'
1228 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229
1230 return remote, upstream_branch
1231
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001232 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001233 upstream_branch = self.GetUpstreamBranch()
1234 if not BranchExists(upstream_branch):
1235 DieWithError('The upstream for the current branch (%s) does not exist '
1236 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001237 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001238 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 def GetUpstreamBranch(self):
1241 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001242 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001244 upstream_branch = upstream_branch.replace('refs/heads/',
1245 'refs/remotes/%s/' % remote)
1246 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1247 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 self.upstream_branch = upstream_branch
1249 return self.upstream_branch
1250
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001251 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001252 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001253 remote, branch = None, self.GetBranch()
1254 seen_branches = set()
1255 while branch not in seen_branches:
1256 seen_branches.add(branch)
1257 remote, branch = self.FetchUpstreamTuple(branch)
1258 branch = ShortBranchName(branch)
1259 if remote != '.' or branch.startswith('refs/remotes'):
1260 break
1261 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001262 remotes = RunGit(['remote'], error_ok=True).split()
1263 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001264 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001265 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001266 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001267 logging.warn('Could not determine which remote this change is '
1268 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001269 else:
1270 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001271 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001272 branch = 'HEAD'
1273 if branch.startswith('refs/remotes'):
1274 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001275 elif branch.startswith('refs/branch-heads/'):
1276 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001277 else:
1278 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001279 return self._remote
1280
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001281 def GitSanityChecks(self, upstream_git_obj):
1282 """Checks git repo status and ensures diff is from local commits."""
1283
sbc@chromium.org79706062015-01-14 21:18:12 +00001284 if upstream_git_obj is None:
1285 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001286 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001287 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001288 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001289 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001290 return False
1291
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001292 # Verify the commit we're diffing against is in our current branch.
1293 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1294 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1295 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001296 print('ERROR: %s is not in the current branch. You may need to rebase '
1297 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001298 return False
1299
1300 # List the commits inside the diff, and verify they are all local.
1301 commits_in_diff = RunGit(
1302 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1303 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1304 remote_branch = remote_branch.strip()
1305 if code != 0:
1306 _, remote_branch = self.GetRemoteBranch()
1307
1308 commits_in_remote = RunGit(
1309 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1310
1311 common_commits = set(commits_in_diff) & set(commits_in_remote)
1312 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001313 print('ERROR: Your diff contains %d commits already in %s.\n'
1314 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1315 'the diff. If you are using a custom git flow, you can override'
1316 ' the reference used for this check with "git config '
1317 'gitcl.remotebranch <git-ref>".' % (
1318 len(common_commits), remote_branch, upstream_git_obj),
1319 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001320 return False
1321 return True
1322
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001323 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001324 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001325
1326 Returns None if it is not set.
1327 """
tandrii5d48c322016-08-18 16:19:37 -07001328 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001329
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 def GetRemoteUrl(self):
1331 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1332
1333 Returns None if there is no remote.
1334 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001335 is_cached, value = self._cached_remote_url
1336 if is_cached:
1337 return value
1338
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001339 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001340 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1341
1342 # If URL is pointing to a local directory, it is probably a git cache.
1343 if os.path.isdir(url):
1344 url = RunGit(['config', 'remote.%s.url' % remote],
1345 error_ok=True,
1346 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001347 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001348 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001350 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001351 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001352 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001353 self.issue = self._GitGetBranchConfigValue(
1354 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001355 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 return self.issue
1357
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358 def GetIssueURL(self):
1359 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001360 issue = self.GetIssue()
1361 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001362 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001363 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001364
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001365 def GetDescription(self, pretty=False, force=False):
1366 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001368 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 self.has_description = True
1370 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001371 # Set width to 72 columns + 2 space indent.
1372 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001374 lines = self.description.splitlines()
1375 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001376 return self.description
1377
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001378 def GetDescriptionFooters(self):
1379 """Returns (non_footer_lines, footers) for the commit message.
1380
1381 Returns:
1382 non_footer_lines (list(str)) - Simple list of description lines without
1383 any footer. The lines do not contain newlines, nor does the list contain
1384 the empty line between the message and the footers.
1385 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1386 [("Change-Id", "Ideadbeef...."), ...]
1387 """
1388 raw_description = self.GetDescription()
1389 msg_lines, _, footers = git_footers.split_footers(raw_description)
1390 if footers:
1391 msg_lines = msg_lines[:len(msg_lines)-1]
1392 return msg_lines, footers
1393
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001395 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001396 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001397 self.patchset = self._GitGetBranchConfigValue(
1398 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001399 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400 return self.patchset
1401
1402 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001403 """Set this branch's patchset. If patchset=0, clears the patchset."""
1404 assert self.GetBranch()
1405 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001406 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001407 else:
1408 self.patchset = int(patchset)
1409 self._GitSetBranchConfigValue(
1410 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001412 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001413 """Set this branch's issue. If issue isn't given, clears the issue."""
1414 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001416 issue = int(issue)
1417 self._GitSetBranchConfigValue(
1418 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001419 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001420 codereview_server = self._codereview_impl.GetCodereviewServer()
1421 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001422 self._GitSetBranchConfigValue(
1423 self._codereview_impl.CodereviewServerConfigKey(),
1424 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 else:
tandrii5d48c322016-08-18 16:19:37 -07001426 # Reset all of these just to be clean.
1427 reset_suffixes = [
1428 'last-upload-hash',
1429 self._codereview_impl.IssueConfigKey(),
1430 self._codereview_impl.PatchsetConfigKey(),
1431 self._codereview_impl.CodereviewServerConfigKey(),
1432 ] + self._PostUnsetIssueProperties()
1433 for prop in reset_suffixes:
1434 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001435 msg = RunGit(['log', '-1', '--format=%B']).strip()
1436 if msg and git_footers.get_footer_change_id(msg):
1437 print('WARNING: The change patched into this branch has a Change-Id. '
1438 'Removing it.')
1439 RunGit(['commit', '--amend', '-m',
1440 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001441 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001442 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443
dnjba1b0f32016-09-02 12:37:42 -07001444 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001445 if not self.GitSanityChecks(upstream_branch):
1446 DieWithError('\nGit sanity check failure')
1447
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001448 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001449 if not root:
1450 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001451 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001452
1453 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001454 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001455 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001456 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001457 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001458 except subprocess2.CalledProcessError:
1459 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001460 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001461 'This branch probably doesn\'t exist anymore. To reset the\n'
1462 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001463 ' git branch --set-upstream-to origin/master %s\n'
1464 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001465 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001466
maruel@chromium.org52424302012-08-29 15:14:30 +00001467 issue = self.GetIssue()
1468 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001469 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001470 description = self.GetDescription()
1471 else:
1472 # If the change was never uploaded, use the log messages of all commits
1473 # up to the branch point, as git cl upload will prefill the description
1474 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001475 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1476 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001477
1478 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001479 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001480 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001481 name,
1482 description,
1483 absroot,
1484 files,
1485 issue,
1486 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001487 author,
1488 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001489
dsansomee2d6fd92016-09-08 00:10:47 -07001490 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001491 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001492 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001493 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001494
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001495 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1496 """Sets the description for this CL remotely.
1497
1498 You can get description_lines and footers with GetDescriptionFooters.
1499
1500 Args:
1501 description_lines (list(str)) - List of CL description lines without
1502 newline characters.
1503 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1504 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1505 `List-Of-Tokens`). It will be case-normalized so that each token is
1506 title-cased.
1507 """
1508 new_description = '\n'.join(description_lines)
1509 if footers:
1510 new_description += '\n'
1511 for k, v in footers:
1512 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1513 if not git_footers.FOOTER_PATTERN.match(foot):
1514 raise ValueError('Invalid footer %r' % foot)
1515 new_description += foot + '\n'
1516 self.UpdateDescription(new_description, force)
1517
Edward Lesmes8e282792018-04-03 18:50:29 -04001518 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001519 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1520 try:
1521 return presubmit_support.DoPresubmitChecks(change, committing,
1522 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1523 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001524 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1525 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001526 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001527 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001528
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001529 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1530 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001531 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1532 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001533 else:
1534 # Assume url.
1535 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1536 urlparse.urlparse(issue_arg))
1537 if not parsed_issue_arg or not parsed_issue_arg.valid:
1538 DieWithError('Failed to parse issue argument "%s". '
1539 'Must be an issue number or a valid URL.' % issue_arg)
1540 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001541 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001542
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001543 def CMDUpload(self, options, git_diff_args, orig_args):
1544 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001545 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001546 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001547 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001548 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001549 else:
1550 if self.GetBranch() is None:
1551 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1552
1553 # Default to diffing against common ancestor of upstream branch
1554 base_branch = self.GetCommonAncestorWithUpstream()
1555 git_diff_args = [base_branch, 'HEAD']
1556
Aaron Gablec4c40d12017-05-22 11:49:53 -07001557
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001558 # Fast best-effort checks to abort before running potentially
1559 # expensive hooks if uploading is likely to fail anyway. Passing these
1560 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001561 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001562 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001563
1564 # Apply watchlists on upload.
1565 change = self.GetChange(base_branch, None)
1566 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1567 files = [f.LocalPath() for f in change.AffectedFiles()]
1568 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001569 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001570
1571 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001572 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001573 # Set the reviewer list now so that presubmit checks can access it.
1574 change_description = ChangeDescription(change.FullDescriptionText())
1575 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001576 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001577 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001578 change)
1579 change.SetDescriptionText(change_description.description)
1580 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001581 may_prompt=not options.force,
1582 verbose=options.verbose,
1583 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001584 if not hook_results.should_continue():
1585 return 1
1586 if not options.reviewers and hook_results.reviewers:
1587 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001588 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001589
Aaron Gable13101a62018-02-09 13:20:41 -08001590 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001591 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001592 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001593 _git_set_branch_config_value('last-upload-hash',
1594 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001595 # Run post upload hooks, if specified.
1596 if settings.GetRunPostUploadHook():
1597 presubmit_support.DoPostUploadExecuter(
1598 change,
1599 self,
1600 settings.GetRoot(),
1601 options.verbose,
1602 sys.stdout)
1603
1604 # Upload all dependencies if specified.
1605 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001606 print()
1607 print('--dependencies has been specified.')
1608 print('All dependent local branches will be re-uploaded.')
1609 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001610 # Remove the dependencies flag from args so that we do not end up in a
1611 # loop.
1612 orig_args.remove('--dependencies')
1613 ret = upload_branch_deps(self, orig_args)
1614 return ret
1615
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001616 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001617 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001618
1619 Issue must have been already uploaded and known.
1620 """
1621 assert new_state in _CQState.ALL_STATES
1622 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001623 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001624 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001625 return 0
1626 except KeyboardInterrupt:
1627 raise
1628 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001629 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001630 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001631 ' * Your project has no CQ,\n'
1632 ' * You don\'t have permission to change the CQ state,\n'
1633 ' * There\'s a bug in this code (see stack trace below).\n'
1634 'Consider specifying which bots to trigger manually or asking your '
1635 'project owners for permissions or contacting Chrome Infra at:\n'
1636 'https://www.chromium.org/infra\n\n' %
1637 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001638 # Still raise exception so that stack trace is printed.
1639 raise
1640
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001641 # Forward methods to codereview specific implementation.
1642
Aaron Gable636b13f2017-07-14 10:42:48 -07001643 def AddComment(self, message, publish=None):
1644 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001645
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001646 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001647 """Returns list of _CommentSummary for each comment.
1648
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001649 args:
1650 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001651 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001652 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001653
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001654 def CloseIssue(self):
1655 return self._codereview_impl.CloseIssue()
1656
1657 def GetStatus(self):
1658 return self._codereview_impl.GetStatus()
1659
1660 def GetCodereviewServer(self):
1661 return self._codereview_impl.GetCodereviewServer()
1662
tandriide281ae2016-10-12 06:02:30 -07001663 def GetIssueOwner(self):
1664 """Get owner from codereview, which may differ from this checkout."""
1665 return self._codereview_impl.GetIssueOwner()
1666
Edward Lemur707d70b2018-02-07 00:50:14 +01001667 def GetReviewers(self):
1668 return self._codereview_impl.GetReviewers()
1669
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001670 def GetMostRecentPatchset(self):
1671 return self._codereview_impl.GetMostRecentPatchset()
1672
tandriide281ae2016-10-12 06:02:30 -07001673 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001674 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001675 return self._codereview_impl.CannotTriggerTryJobReason()
1676
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001677 def GetTryJobProperties(self, patchset=None):
1678 """Returns dictionary of properties to launch try job."""
1679 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001680
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001681 def __getattr__(self, attr):
1682 # This is because lots of untested code accesses Rietveld-specific stuff
1683 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001684 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001685 # Note that child method defines __getattr__ as well, and forwards it here,
1686 # because _RietveldChangelistImpl is not cleaned up yet, and given
1687 # deprecation of Rietveld, it should probably be just removed.
1688 # Until that time, avoid infinite recursion by bypassing __getattr__
1689 # of implementation class.
1690 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001691
1692
1693class _ChangelistCodereviewBase(object):
1694 """Abstract base class encapsulating codereview specifics of a changelist."""
1695 def __init__(self, changelist):
1696 self._changelist = changelist # instance of Changelist
1697
1698 def __getattr__(self, attr):
1699 # Forward methods to changelist.
1700 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1701 # _RietveldChangelistImpl to avoid this hack?
1702 return getattr(self._changelist, attr)
1703
1704 def GetStatus(self):
1705 """Apply a rough heuristic to give a simple summary of an issue's review
1706 or CQ status, assuming adherence to a common workflow.
1707
1708 Returns None if no issue for this branch, or specific string keywords.
1709 """
1710 raise NotImplementedError()
1711
1712 def GetCodereviewServer(self):
1713 """Returns server URL without end slash, like "https://codereview.com"."""
1714 raise NotImplementedError()
1715
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001716 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001717 """Fetches and returns description from the codereview server."""
1718 raise NotImplementedError()
1719
tandrii5d48c322016-08-18 16:19:37 -07001720 @classmethod
1721 def IssueConfigKey(cls):
1722 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001723 raise NotImplementedError()
1724
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001725 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001726 def PatchsetConfigKey(cls):
1727 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001728 raise NotImplementedError()
1729
tandrii5d48c322016-08-18 16:19:37 -07001730 @classmethod
1731 def CodereviewServerConfigKey(cls):
1732 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001733 raise NotImplementedError()
1734
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001735 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001736 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001737 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001738
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001739 def GetGerritObjForPresubmit(self):
1740 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1741 return None
1742
dsansomee2d6fd92016-09-08 00:10:47 -07001743 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001744 """Update the description on codereview site."""
1745 raise NotImplementedError()
1746
Aaron Gable636b13f2017-07-14 10:42:48 -07001747 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001748 """Posts a comment to the codereview site."""
1749 raise NotImplementedError()
1750
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001751 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001752 raise NotImplementedError()
1753
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001754 def CloseIssue(self):
1755 """Closes the issue."""
1756 raise NotImplementedError()
1757
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001758 def GetMostRecentPatchset(self):
1759 """Returns the most recent patchset number from the codereview site."""
1760 raise NotImplementedError()
1761
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001762 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001763 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001764 """Fetches and applies the issue.
1765
1766 Arguments:
1767 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1768 reject: if True, reject the failed patch instead of switching to 3-way
1769 merge. Rietveld only.
1770 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1771 only.
1772 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001773 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001774 """
1775 raise NotImplementedError()
1776
1777 @staticmethod
1778 def ParseIssueURL(parsed_url):
1779 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1780 failed."""
1781 raise NotImplementedError()
1782
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001783 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001784 """Best effort check that user is authenticated with codereview server.
1785
1786 Arguments:
1787 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001788 refresh: whether to attempt to refresh credentials. Ignored if not
1789 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001790 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001791 raise NotImplementedError()
1792
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001793 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001794 """Best effort check that uploading isn't supposed to fail for predictable
1795 reasons.
1796
1797 This method should raise informative exception if uploading shouldn't
1798 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001799
1800 Arguments:
1801 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001802 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001803 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001804
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001805 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001806 """Uploads a change to codereview."""
1807 raise NotImplementedError()
1808
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001809 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001810 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001811
1812 Issue must have been already uploaded and known.
1813 """
1814 raise NotImplementedError()
1815
tandriie113dfd2016-10-11 10:20:12 -07001816 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001817 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001818 raise NotImplementedError()
1819
tandriide281ae2016-10-12 06:02:30 -07001820 def GetIssueOwner(self):
1821 raise NotImplementedError()
1822
Edward Lemur707d70b2018-02-07 00:50:14 +01001823 def GetReviewers(self):
1824 raise NotImplementedError()
1825
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001826 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001827 raise NotImplementedError()
1828
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001829
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001830class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001831 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001832 # auth_config is Rietveld thing, kept here to preserve interface only.
1833 super(_GerritChangelistImpl, self).__init__(changelist)
1834 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001835 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001836 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001837 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001838 # Map from change number (issue) to its detail cache.
1839 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001840
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001841 if codereview_host is not None:
1842 assert not codereview_host.startswith('https://'), codereview_host
1843 self._gerrit_host = codereview_host
1844 self._gerrit_server = 'https://%s' % codereview_host
1845
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001846 def _GetGerritHost(self):
1847 # Lazy load of configs.
1848 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001849 if self._gerrit_host and '.' not in self._gerrit_host:
1850 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1851 # This happens for internal stuff http://crbug.com/614312.
1852 parsed = urlparse.urlparse(self.GetRemoteUrl())
1853 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001854 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07001855 ' Your current remote is: %s' % self.GetRemoteUrl())
1856 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1857 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001858 return self._gerrit_host
1859
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001860 def _GetGitHost(self):
1861 """Returns git host to be used when uploading change to Gerrit."""
1862 return urlparse.urlparse(self.GetRemoteUrl()).netloc
1863
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001864 def GetCodereviewServer(self):
1865 if not self._gerrit_server:
1866 # If we're on a branch then get the server potentially associated
1867 # with that branch.
1868 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001869 self._gerrit_server = self._GitGetBranchConfigValue(
1870 self.CodereviewServerConfigKey())
1871 if self._gerrit_server:
1872 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001873 if not self._gerrit_server:
1874 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1875 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001876 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001877 parts[0] = parts[0] + '-review'
1878 self._gerrit_host = '.'.join(parts)
1879 self._gerrit_server = 'https://%s' % self._gerrit_host
1880 return self._gerrit_server
1881
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001882 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001883 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001884 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001885 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001886 logging.warn('can\'t detect Gerrit project.')
1887 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001888 project = urlparse.urlparse(remote_url).path.strip('/')
1889 if project.endswith('.git'):
1890 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001891 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1892 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1893 # gitiles/git-over-https protocol. E.g.,
1894 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1895 # as
1896 # https://chromium.googlesource.com/v8/v8
1897 if project.startswith('a/'):
1898 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001899 return project
1900
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001901 def _GerritChangeIdentifier(self):
1902 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1903
1904 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001905 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001906 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001907 project = self._GetGerritProject()
1908 if project:
1909 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1910 # Fall back on still unique, but less efficient change number.
1911 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001912
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001913 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001914 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001915 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001916
tandrii5d48c322016-08-18 16:19:37 -07001917 @classmethod
1918 def PatchsetConfigKey(cls):
1919 return 'gerritpatchset'
1920
1921 @classmethod
1922 def CodereviewServerConfigKey(cls):
1923 return 'gerritserver'
1924
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001925 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001926 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001927 if settings.GetGerritSkipEnsureAuthenticated():
1928 # For projects with unusual authentication schemes.
1929 # See http://crbug.com/603378.
1930 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001931
1932 # Check presence of cookies only if using cookies-based auth method.
1933 cookie_auth = gerrit_util.Authenticator.get()
1934 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001935 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001936
1937 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001938 self.GetCodereviewServer()
1939 git_host = self._GetGitHost()
1940 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001941
1942 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1943 git_auth = cookie_auth.get_auth_header(git_host)
1944 if gerrit_auth and git_auth:
1945 if gerrit_auth == git_auth:
1946 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001947 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001948 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001949 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001950 ' %s\n'
1951 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001952 ' Consider running the following command:\n'
1953 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001954 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02001955 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001956 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001957 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001958 cookie_auth.get_new_password_message(git_host)))
1959 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001960 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001961 return
1962 else:
1963 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001964 ([] if gerrit_auth else [self._gerrit_host]) +
1965 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001966 DieWithError('Credentials for the following hosts are required:\n'
1967 ' %s\n'
1968 'These are read from %s (or legacy %s)\n'
1969 '%s' % (
1970 '\n '.join(missing),
1971 cookie_auth.get_gitcookies_path(),
1972 cookie_auth.get_netrc_path(),
1973 cookie_auth.get_new_password_message(git_host)))
1974
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001975 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001976 if not self.GetIssue():
1977 return
1978
1979 # Warm change details cache now to avoid RPCs later, reducing latency for
1980 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001981 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00001982 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001983
1984 status = self._GetChangeDetail()['status']
1985 if status in ('MERGED', 'ABANDONED'):
1986 DieWithError('Change %s has been %s, new uploads are not allowed' %
1987 (self.GetIssueURL(),
1988 'submitted' if status == 'MERGED' else 'abandoned'))
1989
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001990 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1991 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1992 # Apparently this check is not very important? Otherwise get_auth_email
1993 # could have been added to other implementations of Authenticator.
1994 cookies_auth = gerrit_util.Authenticator.get()
1995 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001996 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001997
1998 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001999 if self.GetIssueOwner() == cookies_user:
2000 return
2001 logging.debug('change %s owner is %s, cookies user is %s',
2002 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002003 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002004 # so ask what Gerrit thinks of this user.
2005 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2006 if details['email'] == self.GetIssueOwner():
2007 return
2008 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002009 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002010 'as %s.\n'
2011 'Uploading may fail due to lack of permissions.' %
2012 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2013 confirm_or_exit(action='upload')
2014
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002015 def _PostUnsetIssueProperties(self):
2016 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002017 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002018
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002019 def GetGerritObjForPresubmit(self):
2020 return presubmit_support.GerritAccessor(self._GetGerritHost())
2021
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002022 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002023 """Apply a rough heuristic to give a simple summary of an issue's review
2024 or CQ status, assuming adherence to a common workflow.
2025
2026 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002027 * 'error' - error from review tool (including deleted issues)
2028 * 'unsent' - no reviewers added
2029 * 'waiting' - waiting for review
2030 * 'reply' - waiting for uploader to reply to review
2031 * 'lgtm' - Code-Review label has been set
2032 * 'commit' - in the commit queue
2033 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002034 """
2035 if not self.GetIssue():
2036 return None
2037
2038 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002039 data = self._GetChangeDetail([
2040 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002041 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002042 return 'error'
2043
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002044 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002045 return 'closed'
2046
Aaron Gable9ab38c62017-04-06 14:36:33 -07002047 if data['labels'].get('Commit-Queue', {}).get('approved'):
2048 # The section will have an "approved" subsection if anyone has voted
2049 # the maximum value on the label.
2050 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002051
Aaron Gable9ab38c62017-04-06 14:36:33 -07002052 if data['labels'].get('Code-Review', {}).get('approved'):
2053 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002054
2055 if not data.get('reviewers', {}).get('REVIEWER', []):
2056 return 'unsent'
2057
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002058 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002059 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2060 last_message_author = messages.pop().get('author', {})
2061 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002062 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2063 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002064 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002065 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002066 if last_message_author.get('_account_id') == owner:
2067 # Most recent message was by owner.
2068 return 'waiting'
2069 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002070 # Some reply from non-owner.
2071 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002072
2073 # Somehow there are no messages even though there are reviewers.
2074 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002075
2076 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002077 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002078 patchset = data['revisions'][data['current_revision']]['_number']
2079 self.SetPatchset(patchset)
2080 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002081
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002082 def FetchDescription(self, force=False):
2083 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2084 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002085 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002086 return data['revisions'][current_rev]['commit']['message'].encode(
2087 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002088
dsansomee2d6fd92016-09-08 00:10:47 -07002089 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002090 if gerrit_util.HasPendingChangeEdit(
2091 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002092 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002093 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002094 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002095 'unpublished edit. Either publish the edit in the Gerrit web UI '
2096 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002097
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002098 gerrit_util.DeletePendingChangeEdit(
2099 self._GetGerritHost(), self._GerritChangeIdentifier())
2100 gerrit_util.SetCommitMessage(
2101 self._GetGerritHost(), self._GerritChangeIdentifier(),
2102 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002103
Aaron Gable636b13f2017-07-14 10:42:48 -07002104 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002105 gerrit_util.SetReview(
2106 self._GetGerritHost(), self._GerritChangeIdentifier(),
2107 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002108
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002109 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002110 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002111 messages = self._GetChangeDetail(
2112 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2113 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002114 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002115
2116 # Build dictionary of file comments for easy access and sorting later.
2117 # {author+date: {path: {patchset: {line: url+message}}}}
2118 comments = collections.defaultdict(
2119 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2120 for path, line_comments in file_comments.iteritems():
2121 for comment in line_comments:
2122 if comment.get('tag', '').startswith('autogenerated'):
2123 continue
2124 key = (comment['author']['email'], comment['updated'])
2125 if comment.get('side', 'REVISION') == 'PARENT':
2126 patchset = 'Base'
2127 else:
2128 patchset = 'PS%d' % comment['patch_set']
2129 line = comment.get('line', 0)
2130 url = ('https://%s/c/%s/%s/%s#%s%s' %
2131 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2132 'b' if comment.get('side') == 'PARENT' else '',
2133 str(line) if line else ''))
2134 comments[key][path][patchset][line] = (url, comment['message'])
2135
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002136 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002137 for msg in messages:
2138 # Don't bother showing autogenerated messages.
2139 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2140 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002141 # Gerrit spits out nanoseconds.
2142 assert len(msg['date'].split('.')[-1]) == 9
2143 date = datetime.datetime.strptime(msg['date'][:-3],
2144 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002145 message = msg['message']
2146 key = (msg['author']['email'], msg['date'])
2147 if key in comments:
2148 message += '\n'
2149 for path, patchsets in sorted(comments.get(key, {}).items()):
2150 if readable:
2151 message += '\n%s' % path
2152 for patchset, lines in sorted(patchsets.items()):
2153 for line, (url, content) in sorted(lines.items()):
2154 if line:
2155 line_str = 'Line %d' % line
2156 path_str = '%s:%d:' % (path, line)
2157 else:
2158 line_str = 'File comment'
2159 path_str = '%s:0:' % path
2160 if readable:
2161 message += '\n %s, %s: %s' % (patchset, line_str, url)
2162 message += '\n %s\n' % content
2163 else:
2164 message += '\n%s ' % path_str
2165 message += '\n%s\n' % content
2166
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002167 summary.append(_CommentSummary(
2168 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002169 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002170 sender=msg['author']['email'],
2171 # These could be inferred from the text messages and correlated with
2172 # Code-Review label maximum, however this is not reliable.
2173 # Leaving as is until the need arises.
2174 approval=False,
2175 disapproval=False,
2176 ))
2177 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002178
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002179 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002180 gerrit_util.AbandonChange(
2181 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002182
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002183 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002184 gerrit_util.SubmitChange(
2185 self._GetGerritHost(), self._GerritChangeIdentifier(),
2186 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002188 def _GetChangeDetail(self, options=None, no_cache=False):
2189 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002190
2191 If fresh data is needed, set no_cache=True which will clear cache and
2192 thus new data will be fetched from Gerrit.
2193 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002194 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002195 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002196
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002197 # Optimization to avoid multiple RPCs:
2198 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2199 'CURRENT_COMMIT' not in options):
2200 options.append('CURRENT_COMMIT')
2201
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002202 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002203 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002204 options = [o.upper() for o in options]
2205
2206 # Check in cache first unless no_cache is True.
2207 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002208 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002209 else:
2210 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002211 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002212 # Assumption: data fetched before with extra options is suitable
2213 # for return for a smaller set of options.
2214 # For example, if we cached data for
2215 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2216 # and request is for options=[CURRENT_REVISION],
2217 # THEN we can return prior cached data.
2218 if options_set.issubset(cached_options_set):
2219 return data
2220
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002221 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002222 data = gerrit_util.GetChangeDetail(
2223 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002224 except gerrit_util.GerritError as e:
2225 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002226 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002227 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002228
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002229 self._detail_cache.setdefault(cache_key, []).append(
2230 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002231 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002232
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002233 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002234 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002235 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002236 data = gerrit_util.GetChangeCommit(
2237 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002238 except gerrit_util.GerritError as e:
2239 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002240 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002241 raise
agable32978d92016-11-01 12:55:02 -07002242 return data
2243
Olivier Robin75ee7252018-04-13 10:02:56 +02002244 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002245 if git_common.is_dirty_git_tree('land'):
2246 return 1
tandriid60367b2016-06-22 05:25:12 -07002247 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2248 if u'Commit-Queue' in detail.get('labels', {}):
2249 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002250 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2251 'which can test and land changes for you. '
2252 'Are you sure you wish to bypass it?\n',
2253 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002254
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002255 differs = True
tandriic4344b52016-08-29 06:04:54 -07002256 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002257 # Note: git diff outputs nothing if there is no diff.
2258 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002259 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002260 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002261 if detail['current_revision'] == last_upload:
2262 differs = False
2263 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002264 print('WARNING: Local branch contents differ from latest uploaded '
2265 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002266 if differs:
2267 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002268 confirm_or_exit(
2269 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2270 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002271 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002272 elif not bypass_hooks:
2273 hook_results = self.RunHook(
2274 committing=True,
2275 may_prompt=not force,
2276 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002277 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2278 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002279 if not hook_results.should_continue():
2280 return 1
2281
2282 self.SubmitIssue(wait_for_merge=True)
2283 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002284 links = self._GetChangeCommit().get('web_links', [])
2285 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002286 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002287 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002288 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002289 return 0
2290
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002291 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002292 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002293 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002294 assert not directory
2295 assert parsed_issue_arg.valid
2296
2297 self._changelist.issue = parsed_issue_arg.issue
2298
2299 if parsed_issue_arg.hostname:
2300 self._gerrit_host = parsed_issue_arg.hostname
2301 self._gerrit_server = 'https://%s' % self._gerrit_host
2302
tandriic2405f52016-10-10 08:13:15 -07002303 try:
2304 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002305 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002306 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002307
2308 if not parsed_issue_arg.patchset:
2309 # Use current revision by default.
2310 revision_info = detail['revisions'][detail['current_revision']]
2311 patchset = int(revision_info['_number'])
2312 else:
2313 patchset = parsed_issue_arg.patchset
2314 for revision_info in detail['revisions'].itervalues():
2315 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2316 break
2317 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002318 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002319 (parsed_issue_arg.patchset, self.GetIssue()))
2320
Aaron Gable697a91b2018-01-19 15:20:15 -08002321 remote_url = self._changelist.GetRemoteUrl()
2322 if remote_url.endswith('.git'):
2323 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002324 remote_url = remote_url.rstrip('/')
2325
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002326 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002327 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002328
2329 if remote_url != fetch_info['url']:
2330 DieWithError('Trying to patch a change from %s but this repo appears '
2331 'to be %s.' % (fetch_info['url'], remote_url))
2332
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002333 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002334
Aaron Gable62619a32017-06-16 08:22:09 -07002335 if force:
2336 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2337 print('Checked out commit for change %i patchset %i locally' %
2338 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002339 elif nocommit:
2340 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2341 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002342 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002343 RunGit(['cherry-pick', 'FETCH_HEAD'])
2344 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002345 (parsed_issue_arg.issue, patchset))
2346 print('Note: this created a local commit which does not have '
2347 'the same hash as the one uploaded for review. This will make '
2348 'uploading changes based on top of this branch difficult.\n'
2349 'If you want to do that, use "git cl patch --force" instead.')
2350
Stefan Zagerd08043c2017-10-12 12:07:02 -07002351 if self.GetBranch():
2352 self.SetIssue(parsed_issue_arg.issue)
2353 self.SetPatchset(patchset)
2354 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2355 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2356 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2357 else:
2358 print('WARNING: You are in detached HEAD state.\n'
2359 'The patch has been applied to your checkout, but you will not be '
2360 'able to upload a new patch set to the gerrit issue.\n'
2361 'Try using the \'-b\' option if you would like to work on a '
2362 'branch and/or upload a new patch set.')
2363
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002364 return 0
2365
2366 @staticmethod
2367 def ParseIssueURL(parsed_url):
2368 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2369 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002370 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2371 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002372 # Short urls like https://domain/<issue_number> can be used, but don't allow
2373 # specifying the patchset (you'd 404), but we allow that here.
2374 if parsed_url.path == '/':
2375 part = parsed_url.fragment
2376 else:
2377 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002378 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002379 if match:
2380 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002381 issue=int(match.group(3)),
2382 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002383 hostname=parsed_url.netloc,
2384 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002385 return None
2386
tandrii16e0b4e2016-06-07 10:34:28 -07002387 def _GerritCommitMsgHookCheck(self, offer_removal):
2388 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2389 if not os.path.exists(hook):
2390 return
2391 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2392 # custom developer made one.
2393 data = gclient_utils.FileRead(hook)
2394 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2395 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002396 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002397 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002398 'and may interfere with it in subtle ways.\n'
2399 'We recommend you remove the commit-msg hook.')
2400 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002401 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002402 gclient_utils.rm_file_or_tree(hook)
2403 print('Gerrit commit-msg hook removed.')
2404 else:
2405 print('OK, will keep Gerrit commit-msg hook in place.')
2406
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002407 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002408 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002409 if options.squash and options.no_squash:
2410 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002411
2412 if not options.squash and not options.no_squash:
2413 # Load default for user, repo, squash=true, in this order.
2414 options.squash = settings.GetSquashGerritUploads()
2415 elif options.no_squash:
2416 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002417
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002418 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002419 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002420
Aaron Gableb56ad332017-01-06 15:24:31 -08002421 # This may be None; default fallback value is determined in logic below.
2422 title = options.title
2423
Dominic Battre7d1c4842017-10-27 09:17:28 +02002424 # Extract bug number from branch name.
2425 bug = options.bug
2426 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2427 if not bug and match:
2428 bug = match.group(1)
2429
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002430 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002431 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002432 if self.GetIssue():
2433 # Try to get the message from a previous upload.
2434 message = self.GetDescription()
2435 if not message:
2436 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002437 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002438 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002439 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002440 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002441 # When uploading a subsequent patchset, -m|--message is taken
2442 # as the patchset title if --title was not provided.
2443 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002444 else:
2445 default_title = RunGit(
2446 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002447 if options.force:
2448 title = default_title
2449 else:
2450 title = ask_for_data(
2451 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002452 change_id = self._GetChangeDetail()['change_id']
2453 while True:
2454 footer_change_ids = git_footers.get_footer_change_id(message)
2455 if footer_change_ids == [change_id]:
2456 break
2457 if not footer_change_ids:
2458 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002459 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002460 continue
2461 # There is already a valid footer but with different or several ids.
2462 # Doing this automatically is non-trivial as we don't want to lose
2463 # existing other footers, yet we want to append just 1 desired
2464 # Change-Id. Thus, just create a new footer, but let user verify the
2465 # new description.
2466 message = '%s\n\nChange-Id: %s' % (message, change_id)
2467 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002468 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002469 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002470 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002471 'Please, check the proposed correction to the description, '
2472 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2473 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2474 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002475 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002476 if not options.force:
2477 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002478 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002479 message = change_desc.description
2480 if not message:
2481 DieWithError("Description is empty. Aborting...")
2482 # Continue the while loop.
2483 # Sanity check of this code - we should end up with proper message
2484 # footer.
2485 assert [change_id] == git_footers.get_footer_change_id(message)
2486 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002487 else: # if not self.GetIssue()
2488 if options.message:
2489 message = options.message
2490 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002491 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002492 if options.title:
2493 message = options.title + '\n\n' + message
2494 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002495
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002496 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002497 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002498 # On first upload, patchset title is always this string, while
2499 # --title flag gets converted to first line of message.
2500 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002501 if not change_desc.description:
2502 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002503 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002504 if len(change_ids) > 1:
2505 DieWithError('too many Change-Id footers, at most 1 allowed.')
2506 if not change_ids:
2507 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002508 change_desc.set_description(git_footers.add_footer_change_id(
2509 change_desc.description,
2510 GenerateGerritChangeId(change_desc.description)))
2511 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002512 assert len(change_ids) == 1
2513 change_id = change_ids[0]
2514
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002515 if options.reviewers or options.tbrs or options.add_owners_to:
2516 change_desc.update_reviewers(options.reviewers, options.tbrs,
2517 options.add_owners_to, change)
2518
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002519 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002520 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2521 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002522 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002523 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2524 desc_tempfile.write(change_desc.description)
2525 desc_tempfile.close()
2526 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2527 '-F', desc_tempfile.name]).strip()
2528 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002529 else:
2530 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002531 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002532 if not change_desc.description:
2533 DieWithError("Description is empty. Aborting...")
2534
2535 if not git_footers.get_footer_change_id(change_desc.description):
2536 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002537 change_desc.set_description(
2538 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002539 if options.reviewers or options.tbrs or options.add_owners_to:
2540 change_desc.update_reviewers(options.reviewers, options.tbrs,
2541 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002542 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002543 # For no-squash mode, we assume the remote called "origin" is the one we
2544 # want. It is not worthwhile to support different workflows for
2545 # no-squash mode.
2546 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002547 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2548
2549 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002550 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002551 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2552 ref_to_push)]).splitlines()
2553 if len(commits) > 1:
2554 print('WARNING: This will upload %d commits. Run the following command '
2555 'to see which commits will be uploaded: ' % len(commits))
2556 print('git log %s..%s' % (parent, ref_to_push))
2557 print('You can also use `git squash-branch` to squash these into a '
2558 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002559 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002560
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002561 if options.reviewers or options.tbrs or options.add_owners_to:
2562 change_desc.update_reviewers(options.reviewers, options.tbrs,
2563 options.add_owners_to, change)
2564
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002565 reviewers = sorted(change_desc.get_reviewers())
2566 # Add cc's from the CC_LIST and --cc flag (if any).
2567 if not options.private and not options.no_autocc:
2568 cc = self.GetCCList().split(',')
2569 else:
2570 cc = []
2571 if options.cc:
2572 cc.extend(options.cc)
2573 cc = filter(None, [email.strip() for email in cc])
2574 if change_desc.get_cced():
2575 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002576 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2577 valid_accounts = set(reviewers + cc)
2578 # TODO(crbug/877717): relax this for all hosts.
2579 else:
2580 valid_accounts = gerrit_util.ValidAccounts(
2581 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002582 logging.info('accounts %s are recognized, %s invalid',
2583 sorted(valid_accounts),
2584 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002585
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002586 # Extra options that can be specified at push time. Doc:
2587 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002588 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002589
Aaron Gable844cf292017-06-28 11:32:59 -07002590 # By default, new changes are started in WIP mode, and subsequent patchsets
2591 # don't send email. At any time, passing --send-mail will mark the change
2592 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002593 if options.send_mail:
2594 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002595 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002596 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002597 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002598 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002599 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002600
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002601 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002602 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002603
Aaron Gable9b713dd2016-12-14 16:04:21 -08002604 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002605 # Punctuation and whitespace in |title| must be percent-encoded.
2606 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002607
agablec6787972016-09-09 16:13:34 -07002608 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002609 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002610
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002611 for r in sorted(reviewers):
2612 if r in valid_accounts:
2613 refspec_opts.append('r=%s' % r)
2614 reviewers.remove(r)
2615 else:
2616 # TODO(tandrii): this should probably be a hard failure.
2617 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2618 % r)
2619 for c in sorted(cc):
2620 # refspec option will be rejected if cc doesn't correspond to an
2621 # account, even though REST call to add such arbitrary cc may succeed.
2622 if c in valid_accounts:
2623 refspec_opts.append('cc=%s' % c)
2624 cc.remove(c)
2625
rmistry9eadede2016-09-19 11:22:43 -07002626 if options.topic:
2627 # Documentation on Gerrit topics is here:
2628 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002629 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002630
Edward Lemur687ca902018-12-05 02:30:30 +00002631 if options.enable_auto_submit:
2632 refspec_opts.append('l=Auto-Submit+1')
2633 if options.use_commit_queue:
2634 refspec_opts.append('l=Commit-Queue+2')
2635 elif options.cq_dry_run:
2636 refspec_opts.append('l=Commit-Queue+1')
2637
2638 if change_desc.get_reviewers(tbr_only=True):
2639 score = gerrit_util.GetCodeReviewTbrScore(
2640 self._GetGerritHost(),
2641 self._GetGerritProject())
2642 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002643
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002644 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002645 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002646 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002647 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002648 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2649
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002650 refspec_suffix = ''
2651 if refspec_opts:
2652 refspec_suffix = '%' + ','.join(refspec_opts)
2653 assert ' ' not in refspec_suffix, (
2654 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2655 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2656
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002657 try:
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002658 before_push = time_time()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002659 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00002660 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemuredcefdc2018-11-08 14:41:42 +00002661 print_stdout=True,
Edward Lemur49c8eaf2018-11-07 22:13:12 +00002662 # Flush after every line: useful for seeing progress when running as
2663 # recipe.
2664 filter_fn=lambda _: sys.stdout.flush())
2665 push_returncode = 0
Edward Lemurfec80c42018-11-01 23:14:14 +00002666 except subprocess2.CalledProcessError as e:
2667 push_returncode = e.returncode
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002668 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002669 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002670 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002671 'credential problems:\n'
2672 ' git cl creds-check\n',
2673 change_desc)
Edward Lemurfec80c42018-11-01 23:14:14 +00002674 finally:
2675 metrics.collector.add_repeated('sub_commands', {
2676 'command': 'git push',
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002677 'execution_time': time_time() - before_push,
Edward Lemurfec80c42018-11-01 23:14:14 +00002678 'exit_code': push_returncode,
2679 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2680 })
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002681
2682 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002683 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002684 change_numbers = [m.group(1)
2685 for m in map(regex.match, push_stdout.splitlines())
2686 if m]
2687 if len(change_numbers) != 1:
2688 DieWithError(
2689 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002690 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002691 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002692 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002693
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002694 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002695 # GetIssue() is not set in case of non-squash uploads according to tests.
2696 # TODO(agable): non-squash uploads in git cl should be removed.
2697 gerrit_util.AddReviewers(
2698 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002699 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002700 reviewers, cc,
2701 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002702
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002703 return 0
2704
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002705 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2706 change_desc):
2707 """Computes parent of the generated commit to be uploaded to Gerrit.
2708
2709 Returns revision or a ref name.
2710 """
2711 if custom_cl_base:
2712 # Try to avoid creating additional unintended CLs when uploading, unless
2713 # user wants to take this risk.
2714 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2715 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2716 local_ref_of_target_remote])
2717 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002718 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002719 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2720 'If you proceed with upload, more than 1 CL may be created by '
2721 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2722 'If you are certain that specified base `%s` has already been '
2723 'uploaded to Gerrit as another CL, you may proceed.\n' %
2724 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2725 if not force:
2726 confirm_or_exit(
2727 'Do you take responsibility for cleaning up potential mess '
2728 'resulting from proceeding with upload?',
2729 action='upload')
2730 return custom_cl_base
2731
Aaron Gablef97e33d2017-03-30 15:44:27 -07002732 if remote != '.':
2733 return self.GetCommonAncestorWithUpstream()
2734
2735 # If our upstream branch is local, we base our squashed commit on its
2736 # squashed version.
2737 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2738
Aaron Gablef97e33d2017-03-30 15:44:27 -07002739 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002740 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002741
2742 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002743 # TODO(tandrii): consider checking parent change in Gerrit and using its
2744 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2745 # the tree hash of the parent branch. The upside is less likely bogus
2746 # requests to reupload parent change just because it's uploadhash is
2747 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002748 parent = RunGit(['config',
2749 'branch.%s.gerritsquashhash' % upstream_branch_name],
2750 error_ok=True).strip()
2751 # Verify that the upstream branch has been uploaded too, otherwise
2752 # Gerrit will create additional CLs when uploading.
2753 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2754 RunGitSilent(['rev-parse', parent + ':'])):
2755 DieWithError(
2756 '\nUpload upstream branch %s first.\n'
2757 'It is likely that this branch has been rebased since its last '
2758 'upload, so you just need to upload it again.\n'
2759 '(If you uploaded it with --no-squash, then branch dependencies '
2760 'are not supported, and you should reupload with --squash.)'
2761 % upstream_branch_name,
2762 change_desc)
2763 return parent
2764
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002765 def _AddChangeIdToCommitMessage(self, options, args):
2766 """Re-commits using the current message, assumes the commit hook is in
2767 place.
2768 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002769 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002770 git_command = ['commit', '--amend', '-m', log_desc]
2771 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002772 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002773 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002774 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002775 return new_log_desc
2776 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002777 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002778
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002779 def SetCQState(self, new_state):
2780 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002781 vote_map = {
2782 _CQState.NONE: 0,
2783 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002784 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002785 }
Aaron Gablefc62f762017-07-17 11:12:07 -07002786 labels = {'Commit-Queue': vote_map[new_state]}
2787 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002788 gerrit_util.SetReview(
2789 self._GetGerritHost(), self._GerritChangeIdentifier(),
2790 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002791
tandriie113dfd2016-10-11 10:20:12 -07002792 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002793 try:
2794 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002795 except GerritChangeNotExists:
2796 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002797
2798 if data['status'] in ('ABANDONED', 'MERGED'):
2799 return 'CL %s is closed' % self.GetIssue()
2800
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002801 def GetTryJobProperties(self, patchset=None):
2802 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002803 data = self._GetChangeDetail(['ALL_REVISIONS'])
2804 patchset = int(patchset or self.GetPatchset())
2805 assert patchset
2806 revision_data = None # Pylint wants it to be defined.
2807 for revision_data in data['revisions'].itervalues():
2808 if int(revision_data['_number']) == patchset:
2809 break
2810 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002811 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002812 (patchset, self.GetIssue()))
2813 return {
2814 'patch_issue': self.GetIssue(),
2815 'patch_set': patchset or self.GetPatchset(),
2816 'patch_project': data['project'],
2817 'patch_storage': 'gerrit',
2818 'patch_ref': revision_data['fetch']['http']['ref'],
2819 'patch_repository_url': revision_data['fetch']['http']['url'],
2820 'patch_gerrit_url': self.GetCodereviewServer(),
2821 }
tandriie113dfd2016-10-11 10:20:12 -07002822
tandriide281ae2016-10-12 06:02:30 -07002823 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002824 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002825
Edward Lemur707d70b2018-02-07 00:50:14 +01002826 def GetReviewers(self):
2827 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002828 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002829
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002830
2831_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002832 'gerrit': _GerritChangelistImpl,
2833}
2834
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002835
iannuccie53c9352016-08-17 14:40:40 -07002836def _add_codereview_issue_select_options(parser, extra=""):
2837 _add_codereview_select_options(parser)
2838
2839 text = ('Operate on this issue number instead of the current branch\'s '
2840 'implicit issue.')
2841 if extra:
2842 text += ' '+extra
2843 parser.add_option('-i', '--issue', type=int, help=text)
2844
2845
2846def _process_codereview_issue_select_options(parser, options):
2847 _process_codereview_select_options(parser, options)
2848 if options.issue is not None and not options.forced_codereview:
2849 parser.error('--issue must be specified with either --rietveld or --gerrit')
2850
2851
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002852def _add_codereview_select_options(parser):
2853 """Appends --gerrit and --rietveld options to force specific codereview."""
2854 parser.codereview_group = optparse.OptionGroup(
2855 parser, 'EXPERIMENTAL! Codereview override options')
2856 parser.add_option_group(parser.codereview_group)
2857 parser.codereview_group.add_option(
2858 '--gerrit', action='store_true',
2859 help='Force the use of Gerrit for codereview')
2860 parser.codereview_group.add_option(
2861 '--rietveld', action='store_true',
2862 help='Force the use of Rietveld for codereview')
2863
2864
2865def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00002866 if options.rietveld:
2867 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002868 options.forced_codereview = None
2869 if options.gerrit:
2870 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002871
2872
tandriif9aefb72016-07-01 09:06:51 -07002873def _get_bug_line_values(default_project, bugs):
2874 """Given default_project and comma separated list of bugs, yields bug line
2875 values.
2876
2877 Each bug can be either:
2878 * a number, which is combined with default_project
2879 * string, which is left as is.
2880
2881 This function may produce more than one line, because bugdroid expects one
2882 project per line.
2883
2884 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2885 ['v8:123', 'chromium:789']
2886 """
2887 default_bugs = []
2888 others = []
2889 for bug in bugs.split(','):
2890 bug = bug.strip()
2891 if bug:
2892 try:
2893 default_bugs.append(int(bug))
2894 except ValueError:
2895 others.append(bug)
2896
2897 if default_bugs:
2898 default_bugs = ','.join(map(str, default_bugs))
2899 if default_project:
2900 yield '%s:%s' % (default_project, default_bugs)
2901 else:
2902 yield default_bugs
2903 for other in sorted(others):
2904 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2905 yield other
2906
2907
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002908class ChangeDescription(object):
2909 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002910 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002911 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002912 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002913 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002914 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2915 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
2916 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
2917 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002918
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002919 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002920 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002921
agable@chromium.org42c20792013-09-12 17:34:49 +00002922 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002923 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002924 return '\n'.join(self._description_lines)
2925
2926 def set_description(self, desc):
2927 if isinstance(desc, basestring):
2928 lines = desc.splitlines()
2929 else:
2930 lines = [line.rstrip() for line in desc]
2931 while lines and not lines[0]:
2932 lines.pop(0)
2933 while lines and not lines[-1]:
2934 lines.pop(-1)
2935 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002936
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002937 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
2938 """Rewrites the R=/TBR= line(s) as a single line each.
2939
2940 Args:
2941 reviewers (list(str)) - list of additional emails to use for reviewers.
2942 tbrs (list(str)) - list of additional emails to use for TBRs.
2943 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
2944 the change that are missing OWNER coverage. If this is not None, you
2945 must also pass a value for `change`.
2946 change (Change) - The Change that should be used for OWNERS lookups.
2947 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002948 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002949 assert isinstance(tbrs, list), tbrs
2950
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002951 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07002952 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002953
2954 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002955 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002956
2957 reviewers = set(reviewers)
2958 tbrs = set(tbrs)
2959 LOOKUP = {
2960 'TBR': tbrs,
2961 'R': reviewers,
2962 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002963
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002964 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00002965 regexp = re.compile(self.R_LINE)
2966 matches = [regexp.match(line) for line in self._description_lines]
2967 new_desc = [l for i, l in enumerate(self._description_lines)
2968 if not matches[i]]
2969 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002970
agable@chromium.org42c20792013-09-12 17:34:49 +00002971 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002972
2973 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00002974 for match in matches:
2975 if not match:
2976 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002977 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
2978
2979 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002980 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00002981 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02002982 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002983 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07002984 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002985 LOOKUP[add_owners_to].update(
2986 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002987
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002988 # If any folks ended up in both groups, remove them from tbrs.
2989 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002990
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002991 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
2992 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00002993
2994 # Put the new lines in the description where the old first R= line was.
2995 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2996 if 0 <= line_loc < len(self._description_lines):
2997 if new_tbr_line:
2998 self._description_lines.insert(line_loc, new_tbr_line)
2999 if new_r_line:
3000 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003001 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003002 if new_r_line:
3003 self.append_footer(new_r_line)
3004 if new_tbr_line:
3005 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003006
Aaron Gable3a16ed12017-03-23 10:51:55 -07003007 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003008 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003009 self.set_description([
3010 '# Enter a description of the change.',
3011 '# This will be displayed on the codereview site.',
3012 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003013 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003014 '--------------------',
3015 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003016
agable@chromium.org42c20792013-09-12 17:34:49 +00003017 regexp = re.compile(self.BUG_LINE)
3018 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003019 prefix = settings.GetBugPrefix()
3020 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003021 if git_footer:
3022 self.append_footer('Bug: %s' % ', '.join(values))
3023 else:
3024 for value in values:
3025 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003026
agable@chromium.org42c20792013-09-12 17:34:49 +00003027 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003028 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003029 if not content:
3030 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003031 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003032
Bruce Dawson2377b012018-01-11 16:46:49 -08003033 # Strip off comments and default inserted "Bug:" line.
3034 clean_lines = [line.rstrip() for line in lines if not
3035 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003036 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003037 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003038 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003039
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003040 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003041 """Adds a footer line to the description.
3042
3043 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3044 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3045 that Gerrit footers are always at the end.
3046 """
3047 parsed_footer_line = git_footers.parse_footer(line)
3048 if parsed_footer_line:
3049 # Line is a gerrit footer in the form: Footer-Key: any value.
3050 # Thus, must be appended observing Gerrit footer rules.
3051 self.set_description(
3052 git_footers.add_footer(self.description,
3053 key=parsed_footer_line[0],
3054 value=parsed_footer_line[1]))
3055 return
3056
3057 if not self._description_lines:
3058 self._description_lines.append(line)
3059 return
3060
3061 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3062 if gerrit_footers:
3063 # git_footers.split_footers ensures that there is an empty line before
3064 # actual (gerrit) footers, if any. We have to keep it that way.
3065 assert top_lines and top_lines[-1] == ''
3066 top_lines, separator = top_lines[:-1], top_lines[-1:]
3067 else:
3068 separator = [] # No need for separator if there are no gerrit_footers.
3069
3070 prev_line = top_lines[-1] if top_lines else ''
3071 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3072 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3073 top_lines.append('')
3074 top_lines.append(line)
3075 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003076
tandrii99a72f22016-08-17 14:33:24 -07003077 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003078 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003079 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003080 reviewers = [match.group(2).strip()
3081 for match in matches
3082 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003083 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003084
bradnelsond975b302016-10-23 12:20:23 -07003085 def get_cced(self):
3086 """Retrieves the list of reviewers."""
3087 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3088 cced = [match.group(2).strip() for match in matches if match]
3089 return cleanup_list(cced)
3090
Nodir Turakulov23b82142017-11-16 11:04:25 -08003091 def get_hash_tags(self):
3092 """Extracts and sanitizes a list of Gerrit hashtags."""
3093 subject = (self._description_lines or ('',))[0]
3094 subject = re.sub(
3095 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3096
3097 tags = []
3098 start = 0
3099 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3100 while True:
3101 m = bracket_exp.match(subject, start)
3102 if not m:
3103 break
3104 tags.append(self.sanitize_hash_tag(m.group(1)))
3105 start = m.end()
3106
3107 if not tags:
3108 # Try "Tag: " prefix.
3109 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3110 if m:
3111 tags.append(self.sanitize_hash_tag(m.group(1)))
3112 return tags
3113
3114 @classmethod
3115 def sanitize_hash_tag(cls, tag):
3116 """Returns a sanitized Gerrit hash tag.
3117
3118 A sanitized hashtag can be used as a git push refspec parameter value.
3119 """
3120 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3121
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003122 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3123 """Updates this commit description given the parent.
3124
3125 This is essentially what Gnumbd used to do.
3126 Consult https://goo.gl/WMmpDe for more details.
3127 """
3128 assert parent_msg # No, orphan branch creation isn't supported.
3129 assert parent_hash
3130 assert dest_ref
3131 parent_footer_map = git_footers.parse_footers(parent_msg)
3132 # This will also happily parse svn-position, which GnumbD is no longer
3133 # supporting. While we'd generate correct footers, the verifier plugin
3134 # installed in Gerrit will block such commit (ie git push below will fail).
3135 parent_position = git_footers.get_position(parent_footer_map)
3136
3137 # Cherry-picks may have last line obscuring their prior footers,
3138 # from git_footers perspective. This is also what Gnumbd did.
3139 cp_line = None
3140 if (self._description_lines and
3141 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3142 cp_line = self._description_lines.pop()
3143
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003144 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003145
3146 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3147 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003148 for i, line in enumerate(footer_lines):
3149 k, v = git_footers.parse_footer(line) or (None, None)
3150 if k and k.startswith('Cr-'):
3151 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003152
3153 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003154 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003155 if parent_position[0] == dest_ref:
3156 # Same branch as parent.
3157 number = int(parent_position[1]) + 1
3158 else:
3159 number = 1 # New branch, and extra lineage.
3160 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3161 int(parent_position[1])))
3162
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003163 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3164 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003165
3166 self._description_lines = top_lines
3167 if cp_line:
3168 self._description_lines.append(cp_line)
3169 if self._description_lines[-1] != '':
3170 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003171 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003172
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003173
Aaron Gablea1bab272017-04-11 16:38:18 -07003174def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003175 """Retrieves the reviewers that approved a CL from the issue properties with
3176 messages.
3177
3178 Note that the list may contain reviewers that are not committer, thus are not
3179 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003180
3181 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003182 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003183 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003184 return sorted(
3185 set(
3186 message['sender']
3187 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003188 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003189 )
3190 )
3191
3192
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003193def FindCodereviewSettingsFile(filename='codereview.settings'):
3194 """Finds the given file starting in the cwd and going up.
3195
3196 Only looks up to the top of the repository unless an
3197 'inherit-review-settings-ok' file exists in the root of the repository.
3198 """
3199 inherit_ok_file = 'inherit-review-settings-ok'
3200 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003201 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003202 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3203 root = '/'
3204 while True:
3205 if filename in os.listdir(cwd):
3206 if os.path.isfile(os.path.join(cwd, filename)):
3207 return open(os.path.join(cwd, filename))
3208 if cwd == root:
3209 break
3210 cwd = os.path.dirname(cwd)
3211
3212
3213def LoadCodereviewSettingsFromFile(fileobj):
3214 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003215 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003216
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003217 def SetProperty(name, setting, unset_error_ok=False):
3218 fullname = 'rietveld.' + name
3219 if setting in keyvals:
3220 RunGit(['config', fullname, keyvals[setting]])
3221 else:
3222 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3223
tandrii48df5812016-10-17 03:55:37 -07003224 if not keyvals.get('GERRIT_HOST', False):
3225 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003226 # Only server setting is required. Other settings can be absent.
3227 # In that case, we ignore errors raised during option deletion attempt.
3228 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3229 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3230 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003231 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003232 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3233 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003234 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3235 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003236
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003237 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003238 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003239
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003240 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003241 RunGit(['config', 'gerrit.squash-uploads',
3242 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003243
tandrii@chromium.org28253532016-04-14 13:46:56 +00003244 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003245 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003246 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3247
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003248 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003249 # should be of the form
3250 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3251 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003252 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3253 keyvals['ORIGIN_URL_CONFIG']])
3254
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003255
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003256def urlretrieve(source, destination):
3257 """urllib is broken for SSL connections via a proxy therefore we
3258 can't use urllib.urlretrieve()."""
3259 with open(destination, 'w') as f:
3260 f.write(urllib2.urlopen(source).read())
3261
3262
ukai@chromium.org712d6102013-11-27 00:52:58 +00003263def hasSheBang(fname):
3264 """Checks fname is a #! script."""
3265 with open(fname) as f:
3266 return f.read(2).startswith('#!')
3267
3268
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003269# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3270def DownloadHooks(*args, **kwargs):
3271 pass
3272
3273
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003274def DownloadGerritHook(force):
3275 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003276
3277 Args:
3278 force: True to update hooks. False to install hooks if not present.
3279 """
3280 if not settings.GetIsGerrit():
3281 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003282 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003283 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3284 if not os.access(dst, os.X_OK):
3285 if os.path.exists(dst):
3286 if not force:
3287 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003288 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003289 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003290 if not hasSheBang(dst):
3291 DieWithError('Not a script: %s\n'
3292 'You need to download from\n%s\n'
3293 'into .git/hooks/commit-msg and '
3294 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003295 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3296 except Exception:
3297 if os.path.exists(dst):
3298 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003299 DieWithError('\nFailed to download hooks.\n'
3300 'You need to download from\n%s\n'
3301 'into .git/hooks/commit-msg and '
3302 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003303
3304
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003305class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003306 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003307
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003308 _GOOGLESOURCE = 'googlesource.com'
3309
3310 def __init__(self):
3311 # Cached list of [host, identity, source], where source is either
3312 # .gitcookies or .netrc.
3313 self._all_hosts = None
3314
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003315 def ensure_configured_gitcookies(self):
3316 """Runs checks and suggests fixes to make git use .gitcookies from default
3317 path."""
3318 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3319 configured_path = RunGitSilent(
3320 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003321 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003322 if configured_path:
3323 self._ensure_default_gitcookies_path(configured_path, default)
3324 else:
3325 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003326
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003327 @staticmethod
3328 def _ensure_default_gitcookies_path(configured_path, default_path):
3329 assert configured_path
3330 if configured_path == default_path:
3331 print('git is already configured to use your .gitcookies from %s' %
3332 configured_path)
3333 return
3334
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003335 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003336 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3337 (configured_path, default_path))
3338
3339 if not os.path.exists(configured_path):
3340 print('However, your configured .gitcookies file is missing.')
3341 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3342 action='reconfigure')
3343 RunGit(['config', '--global', 'http.cookiefile', default_path])
3344 return
3345
3346 if os.path.exists(default_path):
3347 print('WARNING: default .gitcookies file already exists %s' %
3348 default_path)
3349 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3350 default_path)
3351
3352 confirm_or_exit('Move existing .gitcookies to default location?',
3353 action='move')
3354 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003355 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003356 print('Moved and reconfigured git to use .gitcookies from %s' %
3357 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003358
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003359 @staticmethod
3360 def _configure_gitcookies_path(default_path):
3361 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3362 if os.path.exists(netrc_path):
3363 print('You seem to be using outdated .netrc for git credentials: %s' %
3364 netrc_path)
3365 print('This tool will guide you through setting up recommended '
3366 '.gitcookies store for git credentials.\n'
3367 '\n'
3368 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3369 ' git config --global --unset http.cookiefile\n'
3370 ' mv %s %s.backup\n\n' % (default_path, default_path))
3371 confirm_or_exit(action='setup .gitcookies')
3372 RunGit(['config', '--global', 'http.cookiefile', default_path])
3373 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003374
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003375 def get_hosts_with_creds(self, include_netrc=False):
3376 if self._all_hosts is None:
3377 a = gerrit_util.CookiesAuthenticator()
3378 self._all_hosts = [
3379 (h, u, s)
3380 for h, u, s in itertools.chain(
3381 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3382 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3383 )
3384 if h.endswith(self._GOOGLESOURCE)
3385 ]
3386
3387 if include_netrc:
3388 return self._all_hosts
3389 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3390
3391 def print_current_creds(self, include_netrc=False):
3392 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3393 if not hosts:
3394 print('No Git/Gerrit credentials found')
3395 return
3396 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3397 header = [('Host', 'User', 'Which file'),
3398 ['=' * l for l in lengths]]
3399 for row in (header + hosts):
3400 print('\t'.join((('%%+%ds' % l) % s)
3401 for l, s in zip(lengths, row)))
3402
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003403 @staticmethod
3404 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003405 """Parses identity "git-<username>.domain" into <username> and domain."""
3406 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003407 # distinguishable from sub-domains. But we do know typical domains:
3408 if identity.endswith('.chromium.org'):
3409 domain = 'chromium.org'
3410 username = identity[:-len('.chromium.org')]
3411 else:
3412 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003413 if username.startswith('git-'):
3414 username = username[len('git-'):]
3415 return username, domain
3416
3417 def _get_usernames_of_domain(self, domain):
3418 """Returns list of usernames referenced by .gitcookies in a given domain."""
3419 identities_by_domain = {}
3420 for _, identity, _ in self.get_hosts_with_creds():
3421 username, domain = self._parse_identity(identity)
3422 identities_by_domain.setdefault(domain, []).append(username)
3423 return identities_by_domain.get(domain)
3424
3425 def _canonical_git_googlesource_host(self, host):
3426 """Normalizes Gerrit hosts (with '-review') to Git host."""
3427 assert host.endswith(self._GOOGLESOURCE)
3428 # Prefix doesn't include '.' at the end.
3429 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3430 if prefix.endswith('-review'):
3431 prefix = prefix[:-len('-review')]
3432 return prefix + '.' + self._GOOGLESOURCE
3433
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003434 def _canonical_gerrit_googlesource_host(self, host):
3435 git_host = self._canonical_git_googlesource_host(host)
3436 prefix = git_host.split('.', 1)[0]
3437 return prefix + '-review.' + self._GOOGLESOURCE
3438
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003439 def _get_counterpart_host(self, host):
3440 assert host.endswith(self._GOOGLESOURCE)
3441 git = self._canonical_git_googlesource_host(host)
3442 gerrit = self._canonical_gerrit_googlesource_host(git)
3443 return git if gerrit == host else gerrit
3444
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003445 def has_generic_host(self):
3446 """Returns whether generic .googlesource.com has been configured.
3447
3448 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3449 """
3450 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3451 if host == '.' + self._GOOGLESOURCE:
3452 return True
3453 return False
3454
3455 def _get_git_gerrit_identity_pairs(self):
3456 """Returns map from canonic host to pair of identities (Git, Gerrit).
3457
3458 One of identities might be None, meaning not configured.
3459 """
3460 host_to_identity_pairs = {}
3461 for host, identity, _ in self.get_hosts_with_creds():
3462 canonical = self._canonical_git_googlesource_host(host)
3463 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3464 idx = 0 if canonical == host else 1
3465 pair[idx] = identity
3466 return host_to_identity_pairs
3467
3468 def get_partially_configured_hosts(self):
3469 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003470 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3471 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3472 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003473
3474 def get_conflicting_hosts(self):
3475 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003476 host
3477 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003478 if None not in (i1, i2) and i1 != i2)
3479
3480 def get_duplicated_hosts(self):
3481 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3482 return set(host for host, count in counters.iteritems() if count > 1)
3483
3484 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3485 'chromium.googlesource.com': 'chromium.org',
3486 'chrome-internal.googlesource.com': 'google.com',
3487 }
3488
3489 def get_hosts_with_wrong_identities(self):
3490 """Finds hosts which **likely** reference wrong identities.
3491
3492 Note: skips hosts which have conflicting identities for Git and Gerrit.
3493 """
3494 hosts = set()
3495 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3496 pair = self._get_git_gerrit_identity_pairs().get(host)
3497 if pair and pair[0] == pair[1]:
3498 _, domain = self._parse_identity(pair[0])
3499 if domain != expected:
3500 hosts.add(host)
3501 return hosts
3502
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003503 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003504 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003505 hosts = sorted(hosts)
3506 assert hosts
3507 if extra_column_func is None:
3508 extras = [''] * len(hosts)
3509 else:
3510 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003511 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3512 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003513 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003514 lines.append(tmpl % he)
3515 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003516
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003517 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003518 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003519 yield ('.googlesource.com wildcard record detected',
3520 ['Chrome Infrastructure team recommends to list full host names '
3521 'explicitly.'],
3522 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003523
3524 dups = self.get_duplicated_hosts()
3525 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003526 yield ('The following hosts were defined twice',
3527 self._format_hosts(dups),
3528 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003529
3530 partial = self.get_partially_configured_hosts()
3531 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003532 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3533 'These hosts are missing',
3534 self._format_hosts(partial, lambda host: 'but %s defined' %
3535 self._get_counterpart_host(host)),
3536 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003537
3538 conflicting = self.get_conflicting_hosts()
3539 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003540 yield ('The following Git hosts have differing credentials from their '
3541 'Gerrit counterparts',
3542 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3543 tuple(self._get_git_gerrit_identity_pairs()[host])),
3544 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003545
3546 wrong = self.get_hosts_with_wrong_identities()
3547 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003548 yield ('These hosts likely use wrong identity',
3549 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3550 (self._get_git_gerrit_identity_pairs()[host][0],
3551 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3552 wrong)
3553
3554 def find_and_report_problems(self):
3555 """Returns True if there was at least one problem, else False."""
3556 found = False
3557 bad_hosts = set()
3558 for title, sublines, hosts in self._find_problems():
3559 if not found:
3560 found = True
3561 print('\n\n.gitcookies problem report:\n')
3562 bad_hosts.update(hosts or [])
3563 print(' %s%s' % (title , (':' if sublines else '')))
3564 if sublines:
3565 print()
3566 print(' %s' % '\n '.join(sublines))
3567 print()
3568
3569 if bad_hosts:
3570 assert found
3571 print(' You can manually remove corresponding lines in your %s file and '
3572 'visit the following URLs with correct account to generate '
3573 'correct credential lines:\n' %
3574 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3575 print(' %s' % '\n '.join(sorted(set(
3576 gerrit_util.CookiesAuthenticator().get_new_password_url(
3577 self._canonical_git_googlesource_host(host))
3578 for host in bad_hosts
3579 ))))
3580 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003581
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003582
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003583@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003584def CMDcreds_check(parser, args):
3585 """Checks credentials and suggests changes."""
3586 _, _ = parser.parse_args(args)
3587
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003588 # Code below checks .gitcookies. Abort if using something else.
3589 authn = gerrit_util.Authenticator.get()
3590 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3591 if isinstance(authn, gerrit_util.GceAuthenticator):
3592 DieWithError(
3593 'This command is not designed for GCE, are you on a bot?\n'
3594 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3595 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003596 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003597 'This command is not designed for bot environment. It checks '
3598 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003599
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003600 checker = _GitCookiesChecker()
3601 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003602
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003603 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003604 checker.print_current_creds(include_netrc=True)
3605
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003606 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003607 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003608 return 0
3609 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003610
3611
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003612@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003613def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003614 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003615 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3616 branch = ShortBranchName(branchref)
3617 _, args = parser.parse_args(args)
3618 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003619 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003620 return RunGit(['config', 'branch.%s.base-url' % branch],
3621 error_ok=False).strip()
3622 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003623 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003624 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3625 error_ok=False).strip()
3626
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003627def color_for_status(status):
3628 """Maps a Changelist status to color, for CMDstatus and other tools."""
3629 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003630 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003631 'waiting': Fore.BLUE,
3632 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003633 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003634 'lgtm': Fore.GREEN,
3635 'commit': Fore.MAGENTA,
3636 'closed': Fore.CYAN,
3637 'error': Fore.WHITE,
3638 }.get(status, Fore.WHITE)
3639
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003640
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003641def get_cl_statuses(changes, fine_grained, max_processes=None):
3642 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003643
3644 If fine_grained is true, this will fetch CL statuses from the server.
3645 Otherwise, simply indicate if there's a matching url for the given branches.
3646
3647 If max_processes is specified, it is used as the maximum number of processes
3648 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3649 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003650
3651 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003652 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003653 if not changes:
3654 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003655
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003656 if not fine_grained:
3657 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003658 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003659 for cl in changes:
3660 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003661 return
3662
3663 # First, sort out authentication issues.
3664 logging.debug('ensuring credentials exist')
3665 for cl in changes:
3666 cl.EnsureAuthenticated(force=False, refresh=True)
3667
3668 def fetch(cl):
3669 try:
3670 return (cl, cl.GetStatus())
3671 except:
3672 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003673 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003674 raise
3675
3676 threads_count = len(changes)
3677 if max_processes:
3678 threads_count = max(1, min(threads_count, max_processes))
3679 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3680
3681 pool = ThreadPool(threads_count)
3682 fetched_cls = set()
3683 try:
3684 it = pool.imap_unordered(fetch, changes).__iter__()
3685 while True:
3686 try:
3687 cl, status = it.next(timeout=5)
3688 except multiprocessing.TimeoutError:
3689 break
3690 fetched_cls.add(cl)
3691 yield cl, status
3692 finally:
3693 pool.close()
3694
3695 # Add any branches that failed to fetch.
3696 for cl in set(changes) - fetched_cls:
3697 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003698
rmistry@google.com2dd99862015-06-22 12:22:18 +00003699
3700def upload_branch_deps(cl, args):
3701 """Uploads CLs of local branches that are dependents of the current branch.
3702
3703 If the local branch dependency tree looks like:
3704 test1 -> test2.1 -> test3.1
3705 -> test3.2
3706 -> test2.2 -> test3.3
3707
3708 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3709 run on the dependent branches in this order:
3710 test2.1, test3.1, test3.2, test2.2, test3.3
3711
3712 Note: This function does not rebase your local dependent branches. Use it when
3713 you make a change to the parent branch that will not conflict with its
3714 dependent branches, and you would like their dependencies updated in
3715 Rietveld.
3716 """
3717 if git_common.is_dirty_git_tree('upload-branch-deps'):
3718 return 1
3719
3720 root_branch = cl.GetBranch()
3721 if root_branch is None:
3722 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3723 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003724 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003725 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3726 'patchset dependencies without an uploaded CL.')
3727
3728 branches = RunGit(['for-each-ref',
3729 '--format=%(refname:short) %(upstream:short)',
3730 'refs/heads'])
3731 if not branches:
3732 print('No local branches found.')
3733 return 0
3734
3735 # Create a dictionary of all local branches to the branches that are dependent
3736 # on it.
3737 tracked_to_dependents = collections.defaultdict(list)
3738 for b in branches.splitlines():
3739 tokens = b.split()
3740 if len(tokens) == 2:
3741 branch_name, tracked = tokens
3742 tracked_to_dependents[tracked].append(branch_name)
3743
vapiera7fbd5a2016-06-16 09:17:49 -07003744 print()
3745 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003746 dependents = []
3747 def traverse_dependents_preorder(branch, padding=''):
3748 dependents_to_process = tracked_to_dependents.get(branch, [])
3749 padding += ' '
3750 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003751 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003752 dependents.append(dependent)
3753 traverse_dependents_preorder(dependent, padding)
3754 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003755 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003756
3757 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003758 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003759 return 0
3760
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003761 confirm_or_exit('This command will checkout all dependent branches and run '
3762 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003763
rmistry@google.com2dd99862015-06-22 12:22:18 +00003764 # Record all dependents that failed to upload.
3765 failures = {}
3766 # Go through all dependents, checkout the branch and upload.
3767 try:
3768 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003769 print()
3770 print('--------------------------------------')
3771 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003772 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003773 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003774 try:
3775 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003776 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003777 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003778 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003779 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003780 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003781 finally:
3782 # Swap back to the original root branch.
3783 RunGit(['checkout', '-q', root_branch])
3784
vapiera7fbd5a2016-06-16 09:17:49 -07003785 print()
3786 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003787 for dependent_branch in dependents:
3788 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003789 print(' %s : %s' % (dependent_branch, upload_status))
3790 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003791
3792 return 0
3793
3794
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003795@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003796def CMDarchive(parser, args):
3797 """Archives and deletes branches associated with closed changelists."""
3798 parser.add_option(
3799 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003800 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003801 parser.add_option(
3802 '-f', '--force', action='store_true',
3803 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003804 parser.add_option(
3805 '-d', '--dry-run', action='store_true',
3806 help='Skip the branch tagging and removal steps.')
3807 parser.add_option(
3808 '-t', '--notags', action='store_true',
3809 help='Do not tag archived branches. '
3810 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003811
3812 auth.add_auth_options(parser)
3813 options, args = parser.parse_args(args)
3814 if args:
3815 parser.error('Unsupported args: %s' % ' '.join(args))
3816 auth_config = auth.extract_auth_config_from_options(options)
3817
3818 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3819 if not branches:
3820 return 0
3821
vapiera7fbd5a2016-06-16 09:17:49 -07003822 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003823 changes = [Changelist(branchref=b, auth_config=auth_config)
3824 for b in branches.splitlines()]
3825 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3826 statuses = get_cl_statuses(changes,
3827 fine_grained=True,
3828 max_processes=options.maxjobs)
3829 proposal = [(cl.GetBranch(),
3830 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3831 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003832 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003833 proposal.sort()
3834
3835 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003836 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003837 return 0
3838
3839 current_branch = GetCurrentBranch()
3840
vapiera7fbd5a2016-06-16 09:17:49 -07003841 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003842 if options.notags:
3843 for next_item in proposal:
3844 print(' ' + next_item[0])
3845 else:
3846 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3847 for next_item in proposal:
3848 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003849
kmarshall9249e012016-08-23 12:02:16 -07003850 # Quit now on precondition failure or if instructed by the user, either
3851 # via an interactive prompt or by command line flags.
3852 if options.dry_run:
3853 print('\nNo changes were made (dry run).\n')
3854 return 0
3855 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003856 print('You are currently on a branch \'%s\' which is associated with a '
3857 'closed codereview issue, so archive cannot proceed. Please '
3858 'checkout another branch and run this command again.' %
3859 current_branch)
3860 return 1
kmarshall9249e012016-08-23 12:02:16 -07003861 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003862 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3863 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003864 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003865 return 1
3866
3867 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003868 if not options.notags:
3869 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003870 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003871
vapiera7fbd5a2016-06-16 09:17:49 -07003872 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003873
3874 return 0
3875
3876
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003877@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003878def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003879 """Show status of changelists.
3880
3881 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003882 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003883 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003884 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003885 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003886 - Magenta in the commit queue
3887 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003888 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003889
3890 Also see 'git cl comments'.
3891 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003892 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003893 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003894 parser.add_option('-f', '--fast', action='store_true',
3895 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003896 parser.add_option(
3897 '-j', '--maxjobs', action='store', type=int,
3898 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003899
3900 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003901 _add_codereview_issue_select_options(
3902 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003903 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003904 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003905 if args:
3906 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003907 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003908
iannuccie53c9352016-08-17 14:40:40 -07003909 if options.issue is not None and not options.field:
3910 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003911
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003912 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003913 cl = Changelist(auth_config=auth_config, issue=options.issue,
3914 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003915 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003916 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003917 elif options.field == 'id':
3918 issueid = cl.GetIssue()
3919 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003920 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003921 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003922 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003923 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003924 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003925 elif options.field == 'status':
3926 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003927 elif options.field == 'url':
3928 url = cl.GetIssueURL()
3929 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003930 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003931 return 0
3932
3933 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3934 if not branches:
3935 print('No local branch found.')
3936 return 0
3937
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003938 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003939 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003940 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003941 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003942 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003943 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003944 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003945
Daniel McArdlea23bf592019-02-12 00:25:12 +00003946 current_branch = GetCurrentBranch()
3947
3948 def FormatBranchName(branch, colorize=False):
3949 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3950 an asterisk when it is the current branch."""
3951
3952 asterisk = ""
3953 color = Fore.RESET
3954 if branch == current_branch:
3955 asterisk = "* "
3956 color = Fore.GREEN
3957 branch_name = ShortBranchName(branch)
3958
3959 if colorize:
3960 return asterisk + color + branch_name + Fore.RESET
3961 else:
3962 return branch_name
3963
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003964 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00003965
3966 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003967 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3968 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003969 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003970 c, status = output.next()
3971 branch_statuses[c.GetBranch()] = status
3972 status = branch_statuses.pop(branch)
3973 url = cl.GetIssueURL()
3974 if url and (not status or status == 'error'):
3975 # The issue probably doesn't exist anymore.
3976 url += ' (broken)'
3977
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003978 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003979 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003980 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003981 color = ''
3982 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003983 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003984 print(' %*s : %s%s %s%s' % (
Daniel McArdlea23bf592019-02-12 00:25:12 +00003985 alignment, FormatBranchName(branch, colorize=True), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003986 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003987
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003988
vapiera7fbd5a2016-06-16 09:17:49 -07003989 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00003990 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003991 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00003992 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003993 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003994 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003995 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003996 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003997 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003998 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003999 print('Issue description:')
4000 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004001 return 0
4002
4003
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004004def colorize_CMDstatus_doc():
4005 """To be called once in main() to add colors to git cl status help."""
4006 colors = [i for i in dir(Fore) if i[0].isupper()]
4007
4008 def colorize_line(line):
4009 for color in colors:
4010 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004011 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004012 indent = len(line) - len(line.lstrip(' ')) + 1
4013 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4014 return line
4015
4016 lines = CMDstatus.__doc__.splitlines()
4017 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4018
4019
phajdan.jre328cf92016-08-22 04:12:17 -07004020def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004021 if path == '-':
4022 json.dump(contents, sys.stdout)
4023 else:
4024 with open(path, 'w') as f:
4025 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004026
4027
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004028@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004029@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004030def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004031 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004032
4033 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004034 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004035 parser.add_option('-r', '--reverse', action='store_true',
4036 help='Lookup the branch(es) for the specified issues. If '
4037 'no issues are specified, all branches with mapped '
4038 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004039 parser.add_option('--json',
4040 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004041 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004042 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004043 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004044
dnj@chromium.org406c4402015-03-03 17:22:28 +00004045 if options.reverse:
4046 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004047 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004048 # Reverse issue lookup.
4049 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004050
4051 git_config = {}
4052 for config in RunGit(['config', '--get-regexp',
4053 r'branch\..*issue']).splitlines():
4054 name, _space, val = config.partition(' ')
4055 git_config[name] = val
4056
dnj@chromium.org406c4402015-03-03 17:22:28 +00004057 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004058 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4059 config_key = _git_branch_config_key(ShortBranchName(branch),
4060 cls.IssueConfigKey())
4061 issue = git_config.get(config_key)
4062 if issue:
4063 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004064 if not args:
4065 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004066 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004067 for issue in args:
4068 if not issue:
4069 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004070 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004071 print('Branch for issue number %s: %s' % (
4072 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004073 if options.json:
4074 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004075 return 0
4076
4077 if len(args) > 0:
4078 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4079 if not issue.valid:
4080 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4081 'or no argument to list it.\n'
4082 'Maybe you want to run git cl status?')
4083 cl = Changelist(codereview=issue.codereview)
4084 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004085 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004086 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004087 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4088 if options.json:
4089 write_json(options.json, {
4090 'issue': cl.GetIssue(),
4091 'issue_url': cl.GetIssueURL(),
4092 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004093 return 0
4094
4095
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004096@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004097def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004098 """Shows or posts review comments for any changelist."""
4099 parser.add_option('-a', '--add-comment', dest='comment',
4100 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004101 parser.add_option('-p', '--publish', action='store_true',
4102 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004103 parser.add_option('-i', '--issue', dest='issue',
4104 help='review issue id (defaults to current issue). '
4105 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004106 parser.add_option('-m', '--machine-readable', dest='readable',
4107 action='store_false', default=True,
4108 help='output comments in a format compatible with '
4109 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004110 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004111 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004112 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004113 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004114 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004115 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004116 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004117
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004118 issue = None
4119 if options.issue:
4120 try:
4121 issue = int(options.issue)
4122 except ValueError:
4123 DieWithError('A review issue id is expected to be a number')
4124
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004125 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4126
4127 if not cl.IsGerrit():
4128 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004129
4130 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004131 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004132 return 0
4133
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004134 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4135 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004136 for comment in summary:
4137 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004138 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004139 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004140 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004141 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004142 color = Fore.MAGENTA
4143 else:
4144 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004145 print('\n%s%s %s%s\n%s' % (
4146 color,
4147 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4148 comment.sender,
4149 Fore.RESET,
4150 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4151
smut@google.comc85ac942015-09-15 16:34:43 +00004152 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004153 def pre_serialize(c):
4154 dct = c.__dict__.copy()
4155 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4156 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004157 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004158 return 0
4159
4160
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004161@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004162@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004163def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004164 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004165 parser.add_option('-d', '--display', action='store_true',
4166 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004167 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004168 help='New description to set for this issue (- for stdin, '
4169 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004170 parser.add_option('-f', '--force', action='store_true',
4171 help='Delete any unpublished Gerrit edits for this issue '
4172 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004173
4174 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004175 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004176 options, args = parser.parse_args(args)
4177 _process_codereview_select_options(parser, options)
4178
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004179 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004180 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004181 target_issue_arg = ParseIssueNumberArgument(args[0],
4182 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004183 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004184 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004185
martiniss6eda05f2016-06-30 10:18:35 -07004186 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004187 'auth_config': auth.extract_auth_config_from_options(options),
4188 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004189 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004190 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004191 if target_issue_arg:
4192 kwargs['issue'] = target_issue_arg.issue
4193 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004194 if target_issue_arg.codereview and not options.forced_codereview:
4195 detected_codereview_from_url = True
4196 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004197
4198 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004199 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004200 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004201 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004202
4203 if detected_codereview_from_url:
4204 logging.info('canonical issue/change URL: %s (type: %s)\n',
4205 cl.GetIssueURL(), target_issue_arg.codereview)
4206
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004207 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004208
smut@google.com34fb6b12015-07-13 20:03:26 +00004209 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004210 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004211 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004212
4213 if options.new_description:
4214 text = options.new_description
4215 if text == '-':
4216 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004217 elif text == '+':
4218 base_branch = cl.GetCommonAncestorWithUpstream()
4219 change = cl.GetChange(base_branch, None, local_description=True)
4220 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004221
4222 description.set_description(text)
4223 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004224 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004225
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004226 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004227 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004228 return 0
4229
4230
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004231@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004232def CMDlint(parser, args):
4233 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004234 parser.add_option('--filter', action='append', metavar='-x,+y',
4235 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004236 auth.add_auth_options(parser)
4237 options, args = parser.parse_args(args)
4238 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004239
4240 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004241 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004242 try:
4243 import cpplint
4244 import cpplint_chromium
4245 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004246 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004247 return 1
4248
4249 # Change the current working directory before calling lint so that it
4250 # shows the correct base.
4251 previous_cwd = os.getcwd()
4252 os.chdir(settings.GetRoot())
4253 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004254 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004255 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4256 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004257 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004258 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004259 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004260
4261 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004262 command = args + files
4263 if options.filter:
4264 command = ['--filter=' + ','.join(options.filter)] + command
4265 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004266
4267 white_regex = re.compile(settings.GetLintRegex())
4268 black_regex = re.compile(settings.GetLintIgnoreRegex())
4269 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4270 for filename in filenames:
4271 if white_regex.match(filename):
4272 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004273 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004274 else:
4275 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4276 extra_check_functions)
4277 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004278 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004279 finally:
4280 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004281 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004282 if cpplint._cpplint_state.error_count != 0:
4283 return 1
4284 return 0
4285
4286
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004287@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004288def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004289 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004290 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004291 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004292 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004293 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004294 parser.add_option('--all', action='store_true',
4295 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004296 parser.add_option('--parallel', action='store_true',
4297 help='Run all tests specified by input_api.RunTests in all '
4298 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004299 auth.add_auth_options(parser)
4300 options, args = parser.parse_args(args)
4301 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004302
sbc@chromium.org71437c02015-04-09 19:29:40 +00004303 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004304 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004305 return 1
4306
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004307 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004308 if args:
4309 base_branch = args[0]
4310 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004311 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004312 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004313
Aaron Gable8076c282017-11-29 14:39:41 -08004314 if options.all:
4315 base_change = cl.GetChange(base_branch, None)
4316 files = [('M', f) for f in base_change.AllFiles()]
4317 change = presubmit_support.GitChange(
4318 base_change.Name(),
4319 base_change.FullDescriptionText(),
4320 base_change.RepositoryRoot(),
4321 files,
4322 base_change.issue,
4323 base_change.patchset,
4324 base_change.author_email,
4325 base_change._upstream)
4326 else:
4327 change = cl.GetChange(base_branch, None)
4328
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004329 cl.RunHook(
4330 committing=not options.upload,
4331 may_prompt=False,
4332 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004333 change=change,
4334 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004335 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004336
4337
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004338def GenerateGerritChangeId(message):
4339 """Returns Ixxxxxx...xxx change id.
4340
4341 Works the same way as
4342 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4343 but can be called on demand on all platforms.
4344
4345 The basic idea is to generate git hash of a state of the tree, original commit
4346 message, author/committer info and timestamps.
4347 """
4348 lines = []
4349 tree_hash = RunGitSilent(['write-tree'])
4350 lines.append('tree %s' % tree_hash.strip())
4351 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4352 if code == 0:
4353 lines.append('parent %s' % parent.strip())
4354 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4355 lines.append('author %s' % author.strip())
4356 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4357 lines.append('committer %s' % committer.strip())
4358 lines.append('')
4359 # Note: Gerrit's commit-hook actually cleans message of some lines and
4360 # whitespace. This code is not doing this, but it clearly won't decrease
4361 # entropy.
4362 lines.append(message)
4363 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4364 stdin='\n'.join(lines))
4365 return 'I%s' % change_hash.strip()
4366
4367
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004368def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004369 """Computes the remote branch ref to use for the CL.
4370
4371 Args:
4372 remote (str): The git remote for the CL.
4373 remote_branch (str): The git remote branch for the CL.
4374 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004375 """
4376 if not (remote and remote_branch):
4377 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004378
wittman@chromium.org455dc922015-01-26 20:15:50 +00004379 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004380 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004381 # refs, which are then translated into the remote full symbolic refs
4382 # below.
4383 if '/' not in target_branch:
4384 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4385 else:
4386 prefix_replacements = (
4387 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4388 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4389 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4390 )
4391 match = None
4392 for regex, replacement in prefix_replacements:
4393 match = re.search(regex, target_branch)
4394 if match:
4395 remote_branch = target_branch.replace(match.group(0), replacement)
4396 break
4397 if not match:
4398 # This is a branch path but not one we recognize; use as-is.
4399 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004400 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4401 # Handle the refs that need to land in different refs.
4402 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004403
wittman@chromium.org455dc922015-01-26 20:15:50 +00004404 # Create the true path to the remote branch.
4405 # Does the following translation:
4406 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4407 # * refs/remotes/origin/master -> refs/heads/master
4408 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4409 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4410 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4411 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4412 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4413 'refs/heads/')
4414 elif remote_branch.startswith('refs/remotes/branch-heads'):
4415 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004416
wittman@chromium.org455dc922015-01-26 20:15:50 +00004417 return remote_branch
4418
4419
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004420def cleanup_list(l):
4421 """Fixes a list so that comma separated items are put as individual items.
4422
4423 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4424 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4425 """
4426 items = sum((i.split(',') for i in l), [])
4427 stripped_items = (i.strip() for i in items)
4428 return sorted(filter(None, stripped_items))
4429
4430
Aaron Gable4db38df2017-11-03 14:59:07 -07004431@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004432@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004433def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004434 """Uploads the current changelist to codereview.
4435
4436 Can skip dependency patchset uploads for a branch by running:
4437 git config branch.branch_name.skip-deps-uploads True
4438 To unset run:
4439 git config --unset branch.branch_name.skip-deps-uploads
4440 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004441
4442 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4443 a bug number, this bug number is automatically populated in the CL
4444 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004445
4446 If subject contains text in square brackets or has "<text>: " prefix, such
4447 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4448 [git-cl] add support for hashtags
4449 Foo bar: implement foo
4450 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004451 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004452 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4453 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004454 parser.add_option('--bypass-watchlists', action='store_true',
4455 dest='bypass_watchlists',
4456 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004457 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004458 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004459 parser.add_option('--message', '-m', dest='message',
4460 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004461 parser.add_option('-b', '--bug',
4462 help='pre-populate the bug number(s) for this issue. '
4463 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004464 parser.add_option('--message-file', dest='message_file',
4465 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004466 parser.add_option('--title', '-t', dest='title',
4467 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004468 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004469 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004470 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004471 parser.add_option('--tbrs',
4472 action='append', default=[],
4473 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004474 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004475 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004476 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004477 parser.add_option('--hashtag', dest='hashtags',
4478 action='append', default=[],
4479 help=('Gerrit hashtag for new CL; '
4480 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004481 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004482 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004483 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004484 help='tell the commit queue to commit this patchset; '
4485 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004486 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004487 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004488 metavar='TARGET',
4489 help='Apply CL to remote ref TARGET. ' +
4490 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004491 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004492 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004493 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004494 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004495 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004496 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004497 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4498 const='TBR', help='add a set of OWNERS to TBR')
4499 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4500 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004501 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4502 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004503 help='Send the patchset to do a CQ dry run right after '
4504 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004505 parser.add_option('--dependencies', action='store_true',
4506 help='Uploads CLs of all the local branches that depend on '
4507 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004508 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4509 help='Sends your change to the CQ after an approval. Only '
4510 'works on repos that have the Auto-Submit label '
4511 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004512 parser.add_option('--parallel', action='store_true',
4513 help='Run all tests specified by input_api.RunTests in all '
4514 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004515
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004516 parser.add_option('--no-autocc', action='store_true',
4517 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004518 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004519 help='Set the review private. This implies --no-autocc.')
4520
rmistry@google.com2dd99862015-06-22 12:22:18 +00004521 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004522 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004523 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004524 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004525 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004526 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004527
sbc@chromium.org71437c02015-04-09 19:29:40 +00004528 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004529 return 1
4530
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004531 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004532 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004533 options.cc = cleanup_list(options.cc)
4534
tandriib80458a2016-06-23 12:20:07 -07004535 if options.message_file:
4536 if options.message:
4537 parser.error('only one of --message and --message-file allowed.')
4538 options.message = gclient_utils.FileRead(options.message_file)
4539 options.message_file = None
4540
tandrii4d0545a2016-07-06 03:56:49 -07004541 if options.cq_dry_run and options.use_commit_queue:
4542 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4543
Aaron Gableedbc4132017-09-11 13:22:28 -07004544 if options.use_commit_queue:
4545 options.send_mail = True
4546
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004547 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4548 settings.GetIsGerrit()
4549
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004550 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004551 if not cl.IsGerrit():
4552 # Error out with instructions for repos not yet configured for Gerrit.
4553 print('=====================================')
4554 print('NOTICE: Rietveld is no longer supported. '
4555 'You can upload changes to Gerrit with')
4556 print(' git cl upload --gerrit')
4557 print('or set Gerrit to be your default code review tool with')
4558 print(' git config gerrit.host true')
4559 print('=====================================')
4560 return 1
4561
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004562 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004563
4564
Francois Dorayd42c6812017-05-30 15:10:20 -04004565@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004566@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004567def CMDsplit(parser, args):
4568 """Splits a branch into smaller branches and uploads CLs.
4569
4570 Creates a branch and uploads a CL for each group of files modified in the
4571 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004572 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004573 the shared OWNERS file.
4574 """
4575 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004576 help="A text file containing a CL description in which "
4577 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004578 parser.add_option("-c", "--comment", dest="comment_file",
4579 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004580 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4581 default=False,
4582 help="List the files and reviewers for each CL that would "
4583 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004584 parser.add_option("--cq-dry-run", action='store_true',
4585 help="If set, will do a cq dry run for each uploaded CL. "
4586 "Please be careful when doing this; more than ~10 CLs "
4587 "has the potential to overload our build "
4588 "infrastructure. Try to upload these not during high "
4589 "load times (usually 11-3 Mountain View time). Email "
4590 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004591 options, _ = parser.parse_args(args)
4592
4593 if not options.description_file:
4594 parser.error('No --description flag specified.')
4595
4596 def WrappedCMDupload(args):
4597 return CMDupload(OptionParser(), args)
4598
4599 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004600 Changelist, WrappedCMDupload, options.dry_run,
4601 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04004602
4603
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004604@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004605@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004606def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004607 """DEPRECATED: Used to commit the current changelist via git-svn."""
4608 message = ('git-cl no longer supports committing to SVN repositories via '
4609 'git-svn. You probably want to use `git cl land` instead.')
4610 print(message)
4611 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004612
4613
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004614# Two special branches used by git cl land.
4615MERGE_BRANCH = 'git-cl-commit'
4616CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4617
4618
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004619@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004620@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004621def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004622 """Commits the current changelist via git.
4623
4624 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4625 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004626 """
4627 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4628 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004629 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004630 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004631 parser.add_option('--parallel', action='store_true',
4632 help='Run all tests specified by input_api.RunTests in all '
4633 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004634 auth.add_auth_options(parser)
4635 (options, args) = parser.parse_args(args)
4636 auth_config = auth.extract_auth_config_from_options(options)
4637
4638 cl = Changelist(auth_config=auth_config)
4639
Robert Iannucci2e73d432018-03-14 01:10:47 -07004640 if not cl.IsGerrit():
4641 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004642
Robert Iannucci2e73d432018-03-14 01:10:47 -07004643 if not cl.GetIssue():
4644 DieWithError('You must upload the change first to Gerrit.\n'
4645 ' If you would rather have `git cl land` upload '
4646 'automatically for you, see http://crbug.com/642759')
4647 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004648 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004649
4650
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004651@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004652@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004653def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004654 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004655 parser.add_option('-b', dest='newbranch',
4656 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004657 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004658 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004659 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004660 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004661 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004662 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004663 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004664 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004665 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004666 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004667
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004668
4669 group = optparse.OptionGroup(
4670 parser,
4671 'Options for continuing work on the current issue uploaded from a '
4672 'different clone (e.g. different machine). Must be used independently '
4673 'from the other options. No issue number should be specified, and the '
4674 'branch must have an issue number associated with it')
4675 group.add_option('--reapply', action='store_true', dest='reapply',
4676 help='Reset the branch and reapply the issue.\n'
4677 'CAUTION: This will undo any local changes in this '
4678 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004679
4680 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004681 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004682 parser.add_option_group(group)
4683
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004684 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004685 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004686 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004687 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004688 auth_config = auth.extract_auth_config_from_options(options)
4689
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004690 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004691 if options.newbranch:
4692 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004693 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004694 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004695
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004696 cl = Changelist(auth_config=auth_config,
4697 codereview=options.forced_codereview)
4698 if not cl.GetIssue():
4699 parser.error('current branch must have an associated issue')
4700
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004701 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004702 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004703 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004704
4705 RunGit(['reset', '--hard', upstream])
4706 if options.pull:
4707 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004708
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004709 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4710 options.directory)
4711
4712 if len(args) != 1 or not args[0]:
4713 parser.error('Must specify issue number or url')
4714
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004715 target_issue_arg = ParseIssueNumberArgument(args[0],
4716 options.forced_codereview)
4717 if not target_issue_arg.valid:
4718 parser.error('invalid codereview url or CL id')
4719
4720 cl_kwargs = {
4721 'auth_config': auth_config,
4722 'codereview_host': target_issue_arg.hostname,
4723 'codereview': options.forced_codereview,
4724 }
4725 detected_codereview_from_url = False
4726 if target_issue_arg.codereview and not options.forced_codereview:
4727 detected_codereview_from_url = True
4728 cl_kwargs['codereview'] = target_issue_arg.codereview
4729 cl_kwargs['issue'] = target_issue_arg.issue
4730
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004731 # We don't want uncommitted changes mixed up with the patch.
4732 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004733 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004734
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004735 if options.newbranch:
4736 if options.force:
4737 RunGit(['branch', '-D', options.newbranch],
4738 stderr=subprocess2.PIPE, error_ok=True)
4739 RunGit(['new-branch', options.newbranch])
4740
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004741 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004742
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004743 if cl.IsGerrit():
4744 if options.reject:
4745 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004746 if options.directory:
4747 parser.error('--directory is not supported with Gerrit codereview.')
4748
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004749 if detected_codereview_from_url:
4750 print('canonical issue/change URL: %s (type: %s)\n' %
4751 (cl.GetIssueURL(), target_issue_arg.codereview))
4752
4753 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07004754 options.nocommit, options.directory,
4755 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004756
4757
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004758def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004759 """Fetches the tree status and returns either 'open', 'closed',
4760 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004761 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004762 if url:
4763 status = urllib2.urlopen(url).read().lower()
4764 if status.find('closed') != -1 or status == '0':
4765 return 'closed'
4766 elif status.find('open') != -1 or status == '1':
4767 return 'open'
4768 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004769 return 'unset'
4770
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004771
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004772def GetTreeStatusReason():
4773 """Fetches the tree status from a json url and returns the message
4774 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004775 url = settings.GetTreeStatusUrl()
4776 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004777 connection = urllib2.urlopen(json_url)
4778 status = json.loads(connection.read())
4779 connection.close()
4780 return status['message']
4781
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004782
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004783@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004784def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004785 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004786 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004787 status = GetTreeStatus()
4788 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004789 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004790 return 2
4791
vapiera7fbd5a2016-06-16 09:17:49 -07004792 print('The tree is %s' % status)
4793 print()
4794 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004795 if status != 'open':
4796 return 1
4797 return 0
4798
4799
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004800@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004801def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004802 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004803 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004804 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004805 '-b', '--bot', action='append',
4806 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4807 'times to specify multiple builders. ex: '
4808 '"-b win_rel -b win_layout". See '
4809 'the try server waterfall for the builders name and the tests '
4810 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004811 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004812 '-B', '--bucket', default='',
4813 help=('Buildbucket bucket to send the try requests.'))
4814 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004815 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004816 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004817 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004818 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004819 help='Revision to use for the try job; default: the revision will '
4820 'be determined by the try recipe that builder runs, which usually '
4821 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004822 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004823 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004824 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004825 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004826 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004827 '--category', default='git_cl_try', help='Specify custom build category.')
4828 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004829 '--project',
4830 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004831 'in recipe to determine to which repository or directory to '
4832 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004833 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004834 '-p', '--property', dest='properties', action='append', default=[],
4835 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004836 'key2=value2 etc. The value will be treated as '
4837 'json if decodable, or as string otherwise. '
4838 'NOTE: using this may make your try job not usable for CQ, '
4839 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004840 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004841 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4842 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004843 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004844 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09004845 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004846 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09004847 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004848 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004849
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004850 if options.master and options.master.startswith('luci.'):
4851 parser.error(
4852 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00004853 # Make sure that all properties are prop=value pairs.
4854 bad_params = [x for x in options.properties if '=' not in x]
4855 if bad_params:
4856 parser.error('Got properties with missing "=": %s' % bad_params)
4857
maruel@chromium.org15192402012-09-06 12:38:29 +00004858 if args:
4859 parser.error('Unknown arguments: %s' % args)
4860
Koji Ishii31c14782018-01-08 17:17:33 +09004861 cl = Changelist(auth_config=auth_config, issue=options.issue,
4862 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00004863 if not cl.GetIssue():
4864 parser.error('Need to upload first')
4865
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004866 if cl.IsGerrit():
4867 # HACK: warm up Gerrit change detail cache to save on RPCs.
4868 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4869
tandriie113dfd2016-10-11 10:20:12 -07004870 error_message = cl.CannotTriggerTryJobReason()
4871 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004872 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004873
borenet6c0efe62016-10-19 08:13:29 -07004874 if options.bucket and options.master:
4875 parser.error('Only one of --bucket and --master may be used.')
4876
qyearsley1fdfcb62016-10-24 13:22:03 -07004877 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004878
qyearsleydd49f942016-10-28 11:57:22 -07004879 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4880 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004881 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004882 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004883 print('git cl try with no bots now defaults to CQ dry run.')
4884 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4885 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004886
borenet6c0efe62016-10-19 08:13:29 -07004887 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004888 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004889 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004890 'of bot requires an initial job from a parent (usually a builder). '
4891 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004892 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004893 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004894
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004895 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07004896 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004897 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07004898 except BuildbucketResponseException as ex:
4899 print('ERROR: %s' % ex)
4900 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004901 return 0
4902
4903
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004904@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004905def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004906 """Prints info about try jobs associated with current CL."""
4907 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004908 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004909 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004910 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004911 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004912 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004913 '--color', action='store_true', default=setup_color.IS_TTY,
4914 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004915 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004916 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4917 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004918 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07004919 '--json', help=('Path of JSON output file to write try job results to,'
4920 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004921 parser.add_option_group(group)
4922 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07004923 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004924 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07004925 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004926 if args:
4927 parser.error('Unrecognized args: %s' % ' '.join(args))
4928
4929 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07004930 cl = Changelist(
4931 issue=options.issue, codereview=options.forced_codereview,
4932 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004933 if not cl.GetIssue():
4934 parser.error('Need to upload first')
4935
tandrii221ab252016-10-06 08:12:04 -07004936 patchset = options.patchset
4937 if not patchset:
4938 patchset = cl.GetMostRecentPatchset()
4939 if not patchset:
4940 parser.error('Codereview doesn\'t know about issue %s. '
4941 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004942 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07004943 cl.GetIssue())
4944
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004945 try:
tandrii221ab252016-10-06 08:12:04 -07004946 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004947 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004948 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004949 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004950 if options.json:
4951 write_try_results_json(options.json, jobs)
4952 else:
4953 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004954 return 0
4955
4956
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004957@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004958@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004959def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004960 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004961 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004962 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004963 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004964
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004965 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004966 if args:
4967 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004968 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004969 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004970 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004971 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004972
4973 # Clear configured merge-base, if there is one.
4974 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004975 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004976 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004977 return 0
4978
4979
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004980@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00004981def CMDweb(parser, args):
4982 """Opens the current CL in the web browser."""
4983 _, args = parser.parse_args(args)
4984 if args:
4985 parser.error('Unrecognized args: %s' % ' '.join(args))
4986
4987 issue_url = Changelist().GetIssueURL()
4988 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004989 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004990 return 1
4991
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004992 # Redirect I/O before invoking browser to hide its output. For example, this
4993 # allows to hide "Created new window in existing browser session." message
4994 # from Chrome. Based on https://stackoverflow.com/a/2323563.
4995 saved_stdout = os.dup(1)
4996 os.close(1)
4997 os.open(os.devnull, os.O_RDWR)
4998 try:
4999 webbrowser.open(issue_url)
5000 finally:
5001 os.dup2(saved_stdout, 1)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005002 return 0
5003
5004
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005005@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005006def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005007 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005008 parser.add_option('-d', '--dry-run', action='store_true',
5009 help='trigger in dry run mode')
5010 parser.add_option('-c', '--clear', action='store_true',
5011 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005012 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005013 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005014 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005015 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005016 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005017 if args:
5018 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005019 if options.dry_run and options.clear:
5020 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5021
iannuccie53c9352016-08-17 14:40:40 -07005022 cl = Changelist(auth_config=auth_config, issue=options.issue,
5023 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005024 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005025 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005026 elif options.dry_run:
5027 state = _CQState.DRY_RUN
5028 else:
5029 state = _CQState.COMMIT
5030 if not cl.GetIssue():
5031 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005032 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005033 return 0
5034
5035
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005036@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005037def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005038 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005039 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005040 auth.add_auth_options(parser)
5041 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005042 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005043 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005044 if args:
5045 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005046 cl = Changelist(auth_config=auth_config, issue=options.issue,
5047 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005048 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005049 if not cl.GetIssue():
5050 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005051 cl.CloseIssue()
5052 return 0
5053
5054
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005055@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005056def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005057 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005058 parser.add_option(
5059 '--stat',
5060 action='store_true',
5061 dest='stat',
5062 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005063 auth.add_auth_options(parser)
5064 options, args = parser.parse_args(args)
5065 auth_config = auth.extract_auth_config_from_options(options)
5066 if args:
5067 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005068
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005069 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005070 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005071 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005072 if not issue:
5073 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005074
Aaron Gablea718c3e2017-08-28 17:47:28 -07005075 base = cl._GitGetBranchConfigValue('last-upload-hash')
5076 if not base:
5077 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5078 if not base:
5079 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5080 revision_info = detail['revisions'][detail['current_revision']]
5081 fetch_info = revision_info['fetch']['http']
5082 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5083 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005084
Aaron Gablea718c3e2017-08-28 17:47:28 -07005085 cmd = ['git', 'diff']
5086 if options.stat:
5087 cmd.append('--stat')
5088 cmd.append(base)
5089 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005090
5091 return 0
5092
5093
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005094@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005095def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005096 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005097 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005098 '--ignore-current',
5099 action='store_true',
5100 help='Ignore the CL\'s current reviewers and start from scratch.')
5101 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005102 '--ignore-self',
5103 action='store_true',
5104 help='Do not consider CL\'s author as an owners.')
5105 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005106 '--no-color',
5107 action='store_true',
5108 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005109 parser.add_option(
5110 '--batch',
5111 action='store_true',
5112 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005113 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005114 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005115 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005116
5117 author = RunGit(['config', 'user.email']).strip() or None
5118
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005119 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005120
5121 if args:
5122 if len(args) > 1:
5123 parser.error('Unknown args')
5124 base_branch = args[0]
5125 else:
5126 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005127 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005128
5129 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005130 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5131
5132 if options.batch:
5133 db = owners.Database(change.RepositoryRoot(), file, os.path)
5134 print('\n'.join(db.reviewers_for(affected_files, author)))
5135 return 0
5136
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005137 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005138 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005139 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005140 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005141 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005142 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005143 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005144 override_files=change.OriginalOwnersFiles(),
5145 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005146
5147
Aiden Bennerc08566e2018-10-03 17:52:42 +00005148def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005149 """Generates a diff command."""
5150 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005151 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5152
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005153 if allow_prefix:
5154 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5155 # case that diff.noprefix is set in the user's git config.
5156 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5157 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005158 diff_cmd += ['--no-prefix']
5159
5160 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005161
5162 if args:
5163 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005164 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005165 diff_cmd.append(arg)
5166 else:
5167 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005168
5169 return diff_cmd
5170
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005171
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005172def MatchingFileType(file_name, extensions):
5173 """Returns true if the file name ends with one of the given extensions."""
5174 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005175
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005176
enne@chromium.org555cfe42014-01-29 18:21:39 +00005177@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005178@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005179def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005180 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005181 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005182 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005183 parser.add_option('--full', action='store_true',
5184 help='Reformat the full content of all touched files')
5185 parser.add_option('--dry-run', action='store_true',
5186 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005187 parser.add_option(
5188 '--python',
5189 action='store_true',
5190 default=None,
5191 help='Enables python formatting on all python files.')
5192 parser.add_option(
5193 '--no-python',
5194 action='store_true',
5195 dest='python',
5196 help='Disables python formatting on all python files. '
5197 'Takes precedence over --python. '
5198 'If neither --python or --no-python are set, python '
5199 'files that have a .style.yapf file in an ancestor '
5200 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005201 parser.add_option('--js', action='store_true',
5202 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005203 parser.add_option('--diff', action='store_true',
5204 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005205 parser.add_option('--presubmit', action='store_true',
5206 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005207 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005208
Daniel Chengc55eecf2016-12-30 03:11:02 -08005209 # Normalize any remaining args against the current path, so paths relative to
5210 # the current directory are still resolved as expected.
5211 args = [os.path.join(os.getcwd(), arg) for arg in args]
5212
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005213 # git diff generates paths against the root of the repository. Change
5214 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005215 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005216 if rel_base_path:
5217 os.chdir(rel_base_path)
5218
digit@chromium.org29e47272013-05-17 17:01:46 +00005219 # Grab the merge-base commit, i.e. the upstream commit of the current
5220 # branch when it was created or the last time it was rebased. This is
5221 # to cover the case where the user may have called "git fetch origin",
5222 # moving the origin branch to a newer commit, but hasn't rebased yet.
5223 upstream_commit = None
5224 cl = Changelist()
5225 upstream_branch = cl.GetUpstreamBranch()
5226 if upstream_branch:
5227 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5228 upstream_commit = upstream_commit.strip()
5229
5230 if not upstream_commit:
5231 DieWithError('Could not find base commit for this branch. '
5232 'Are you in detached state?')
5233
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005234 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5235 diff_output = RunGit(changed_files_cmd)
5236 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005237 # Filter out files deleted by this CL
5238 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005239
Christopher Lamc5ba6922017-01-24 11:19:14 +11005240 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005241 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005242
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005243 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5244 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5245 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005246 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005247
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005248 top_dir = os.path.normpath(
5249 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5250
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005251 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5252 # formatted. This is used to block during the presubmit.
5253 return_value = 0
5254
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005255 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005256 # Locate the clang-format binary in the checkout
5257 try:
5258 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005259 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005260 DieWithError(e)
5261
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005262 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005263 cmd = [clang_format_tool]
5264 if not opts.dry_run and not opts.diff:
5265 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005266 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005267 if opts.diff:
5268 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005269 else:
5270 env = os.environ.copy()
5271 env['PATH'] = str(os.path.dirname(clang_format_tool))
5272 try:
5273 script = clang_format.FindClangFormatScriptInChromiumTree(
5274 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005275 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005276 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005277
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005278 cmd = [sys.executable, script, '-p0']
5279 if not opts.dry_run and not opts.diff:
5280 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005281
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005282 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5283 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005284
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005285 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5286 if opts.diff:
5287 sys.stdout.write(stdout)
5288 if opts.dry_run and len(stdout) > 0:
5289 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005290
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005291 # Similar code to above, but using yapf on .py files rather than clang-format
5292 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005293 py_explicitly_disabled = opts.python is not None and not opts.python
5294 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005295 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5296 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5297 if sys.platform.startswith('win'):
5298 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005299
Aiden Bennerc08566e2018-10-03 17:52:42 +00005300 # If we couldn't find a yapf file we'll default to the chromium style
5301 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005302 chromium_default_yapf_style = os.path.join(depot_tools_path,
5303 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005304 # Used for caching.
5305 yapf_configs = {}
5306 for f in python_diff_files:
5307 # Find the yapf style config for the current file, defaults to depot
5308 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005309 _FindYapfConfigFile(f, yapf_configs, top_dir)
5310
5311 # Turn on python formatting by default if a yapf config is specified.
5312 # This breaks in the case of this repo though since the specified
5313 # style file is also the global default.
5314 if opts.python is None:
5315 filtered_py_files = []
5316 for f in python_diff_files:
5317 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5318 filtered_py_files.append(f)
5319 else:
5320 filtered_py_files = python_diff_files
5321
5322 # Note: yapf still seems to fix indentation of the entire file
5323 # even if line ranges are specified.
5324 # See https://github.com/google/yapf/issues/499
5325 if not opts.full and filtered_py_files:
5326 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5327
5328 for f in filtered_py_files:
5329 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5330 if yapf_config is None:
5331 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005332
5333 cmd = [yapf_tool, '--style', yapf_config, f]
5334
5335 has_formattable_lines = False
5336 if not opts.full:
5337 # Only run yapf over changed line ranges.
5338 for diff_start, diff_len in py_line_diffs[f]:
5339 diff_end = diff_start + diff_len - 1
5340 # Yapf errors out if diff_end < diff_start but this
5341 # is a valid line range diff for a removal.
5342 if diff_end >= diff_start:
5343 has_formattable_lines = True
5344 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5345 # If all line diffs were removals we have nothing to format.
5346 if not has_formattable_lines:
5347 continue
5348
5349 if opts.diff or opts.dry_run:
5350 cmd += ['--diff']
5351 # Will return non-zero exit code if non-empty diff.
5352 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5353 if opts.diff:
5354 sys.stdout.write(stdout)
5355 elif len(stdout) > 0:
5356 return_value = 2
5357 else:
5358 cmd += ['-i']
5359 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005360
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005361 # Dart's formatter does not have the nice property of only operating on
5362 # modified chunks, so hard code full.
5363 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005364 try:
5365 command = [dart_format.FindDartFmtToolInChromiumTree()]
5366 if not opts.dry_run and not opts.diff:
5367 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005368 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005369
ppi@chromium.org6593d932016-03-03 15:41:15 +00005370 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005371 if opts.dry_run and stdout:
5372 return_value = 2
5373 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005374 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5375 'found in this checkout. Files in other languages are still '
5376 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005377
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005378 # Format GN build files. Always run on full build files for canonical form.
5379 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005380 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005381 if opts.dry_run or opts.diff:
5382 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005383 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005384 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5385 shell=sys.platform == 'win32',
5386 cwd=top_dir)
5387 if opts.dry_run and gn_ret == 2:
5388 return_value = 2 # Not formatted.
5389 elif opts.diff and gn_ret == 2:
5390 # TODO this should compute and print the actual diff.
5391 print("This change has GN build file diff for " + gn_diff_file)
5392 elif gn_ret != 0:
5393 # For non-dry run cases (and non-2 return values for dry-run), a
5394 # nonzero error code indicates a failure, probably because the file
5395 # doesn't parse.
5396 DieWithError("gn format failed on " + gn_diff_file +
5397 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005398
Ilya Shermane081cbe2017-08-15 17:51:04 -07005399 # Skip the metrics formatting from the global presubmit hook. These files have
5400 # a separate presubmit hook that issues an error if the files need formatting,
5401 # whereas the top-level presubmit script merely issues a warning. Formatting
5402 # these files is somewhat slow, so it's important not to duplicate the work.
5403 if not opts.presubmit:
5404 for xml_dir in GetDirtyMetricsDirs(diff_files):
5405 tool_dir = os.path.join(top_dir, xml_dir)
5406 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5407 if opts.dry_run or opts.diff:
5408 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005409 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005410 if opts.diff:
5411 sys.stdout.write(stdout)
5412 if opts.dry_run and stdout:
5413 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005414
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005415 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005416
Steven Holte2e664bf2017-04-21 13:10:47 -07005417def GetDirtyMetricsDirs(diff_files):
5418 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5419 metrics_xml_dirs = [
5420 os.path.join('tools', 'metrics', 'actions'),
5421 os.path.join('tools', 'metrics', 'histograms'),
5422 os.path.join('tools', 'metrics', 'rappor'),
5423 os.path.join('tools', 'metrics', 'ukm')]
5424 for xml_dir in metrics_xml_dirs:
5425 if any(file.startswith(xml_dir) for file in xml_diff_files):
5426 yield xml_dir
5427
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005428
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005429@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005430@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005431def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005432 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005433 _, args = parser.parse_args(args)
5434
5435 if len(args) != 1:
5436 parser.print_help()
5437 return 1
5438
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005439 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005440 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005441 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005442
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005443 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005444
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005445 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005446 output = RunGit(['config', '--local', '--get-regexp',
5447 r'branch\..*\.%s' % issueprefix],
5448 error_ok=True)
5449 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005450 if issue == target_issue:
5451 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005452
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005453 branches = []
5454 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005455 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005456 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005457 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005458 return 1
5459 if len(branches) == 1:
5460 RunGit(['checkout', branches[0]])
5461 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005462 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005463 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005464 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005465 which = raw_input('Choose by index: ')
5466 try:
5467 RunGit(['checkout', branches[int(which)]])
5468 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005469 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005470 return 1
5471
5472 return 0
5473
5474
maruel@chromium.org29404b52014-09-08 22:58:00 +00005475def CMDlol(parser, args):
5476 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005477 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005478 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5479 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5480 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005481 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005482 return 0
5483
5484
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005485class OptionParser(optparse.OptionParser):
5486 """Creates the option parse and add --verbose support."""
5487 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005488 optparse.OptionParser.__init__(
5489 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005490 self.add_option(
5491 '-v', '--verbose', action='count', default=0,
5492 help='Use 2 times for more debugging info')
5493
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005494 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005495 try:
5496 return self._parse_args(args)
5497 finally:
5498 # Regardless of success or failure of args parsing, we want to report
5499 # metrics, but only after logging has been initialized (if parsing
5500 # succeeded).
5501 global settings
5502 settings = Settings()
5503
5504 if not metrics.DISABLE_METRICS_COLLECTION:
5505 # GetViewVCUrl ultimately calls logging method.
5506 project_url = settings.GetViewVCUrl().strip('/+')
5507 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5508 metrics.collector.add('project_urls', [project_url])
5509
5510 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005511 # Create an optparse.Values object that will store only the actual passed
5512 # options, without the defaults.
5513 actual_options = optparse.Values()
5514 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5515 # Create an optparse.Values object with the default options.
5516 options = optparse.Values(self.get_default_values().__dict__)
5517 # Update it with the options passed by the user.
5518 options._update_careful(actual_options.__dict__)
5519 # Store the options passed by the user in an _actual_options attribute.
5520 # We store only the keys, and not the values, since the values can contain
5521 # arbitrary information, which might be PII.
5522 metrics.collector.add('arguments', actual_options.__dict__.keys())
5523
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005524 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005525 logging.basicConfig(
5526 level=levels[min(options.verbose, len(levels) - 1)],
5527 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5528 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005529
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005530 return options, args
5531
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005532
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005533def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005534 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005535 print('\nYour python version %s is unsupported, please upgrade.\n' %
5536 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005537 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005538
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005539 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005540 dispatcher = subcommand.CommandDispatcher(__name__)
5541 try:
5542 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005543 except auth.AuthenticationError as e:
5544 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005545 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005546 if e.code != 500:
5547 raise
5548 DieWithError(
5549 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5550 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005551 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005552
5553
5554if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005555 # These affect sys.stdout so do it outside of main() to simplify mocks in
5556 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005557 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005558 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005559 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005560 sys.exit(main(sys.argv[1:]))