blob: 14c4cd698c967c125cb6392e41d960109c6941bf [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
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000048import auth
skobes6468b902016-10-24 08:45:10 -070049import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000050import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000051import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000052import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000053import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000054import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000055import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000056import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000057import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000058import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000059import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000060import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000061import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000062import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000064import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040066import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000067import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000068import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000069import watchlists
70
tandrii7400cf02016-06-21 08:48:07 -070071__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000072
tandrii9d2c7a32016-06-22 03:42:45 -070073COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070074DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080075POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000077REFS_THAT_ALIAS_TO_OTHER_REFS = {
78 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
79 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
80}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000081
thestig@chromium.org44202a22014-03-11 19:22:18 +000082# Valid extensions for files we want to lint.
83DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
84DEFAULT_LINT_IGNORE_REGEX = r"$^"
85
Aiden Bennerc08566e2018-10-03 17:52:42 +000086# File name for yapf style config files.
87YAPF_CONFIG_FILENAME = '.style.yapf'
88
borenet6c0efe62016-10-19 08:13:29 -070089# Buildbucket master name prefix.
90MASTER_PREFIX = 'master.'
91
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000092# Shortcut since it quickly becomes redundant.
93Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000094
maruel@chromium.orgddd59412011-11-30 14:20:38 +000095# Initialized in main()
96settings = None
97
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010098# Used by tests/git_cl_test.py to add extra logging.
99# Inside the weirdly failing test, add this:
100# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700101# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100102_IS_BEING_TESTED = False
103
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000104
Christopher Lamf732cd52017-01-24 12:40:11 +1100105def DieWithError(message, change_desc=None):
106 if change_desc:
107 SaveDescriptionBackup(change_desc)
108
vapiera7fbd5a2016-06-16 09:17:49 -0700109 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110 sys.exit(1)
111
112
Christopher Lamf732cd52017-01-24 12:40:11 +1100113def SaveDescriptionBackup(change_desc):
114 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000115 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100116 backup_file = open(backup_path, 'w')
117 backup_file.write(change_desc.description)
118 backup_file.close()
119
120
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000121def GetNoGitPagerEnv():
122 env = os.environ.copy()
123 # 'cat' is a magical git string that disables pagers on all platforms.
124 env['GIT_PAGER'] = 'cat'
125 return env
126
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000127
bsep@chromium.org627d9002016-04-29 00:00:52 +0000128def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000130 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000131 except subprocess2.CalledProcessError as e:
132 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000133 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000134 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000135 'Command "%s" failed.\n%s' % (
136 ' '.join(args), error_message or e.stdout or ''))
137 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000138
139
140def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000141 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000142 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000143
144
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000145def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000146 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700147 if suppress_stderr:
148 stderr = subprocess2.VOID
149 else:
150 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000151 try:
tandrii5d48c322016-08-18 16:19:37 -0700152 (out, _), code = subprocess2.communicate(['git'] + args,
153 env=GetNoGitPagerEnv(),
154 stdout=subprocess2.PIPE,
155 stderr=stderr)
156 return code, out
157 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900158 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700159 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000160
161
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000162def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000163 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000164 return RunGitWithCode(args, suppress_stderr=True)[1]
165
166
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000167def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000168 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000169 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000170 return (version.startswith(prefix) and
171 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000172
173
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000174def BranchExists(branch):
175 """Return True if specified branch exists."""
176 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
177 suppress_stderr=True)
178 return not code
179
180
tandrii2a16b952016-10-19 07:09:44 -0700181def time_sleep(seconds):
182 # Use this so that it can be mocked in tests without interfering with python
183 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700184 return time.sleep(seconds)
185
186
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000187def time_time():
188 # Use this so that it can be mocked in tests without interfering with python
189 # system machinery.
190 return time.time()
191
192
maruel@chromium.org90541732011-04-01 17:54:18 +0000193def ask_for_data(prompt):
194 try:
195 return raw_input(prompt)
196 except KeyboardInterrupt:
197 # Hide the exception.
198 sys.exit(1)
199
200
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100201def confirm_or_exit(prefix='', action='confirm'):
202 """Asks user to press enter to continue or press Ctrl+C to abort."""
203 if not prefix or prefix.endswith('\n'):
204 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100205 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100206 mid = ' Press'
207 elif prefix.endswith(' '):
208 mid = 'press'
209 else:
210 mid = ' press'
211 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
212
213
214def ask_for_explicit_yes(prompt):
215 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
216 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
217 while True:
218 if 'yes'.startswith(result):
219 return True
220 if 'no'.startswith(result):
221 return False
222 result = ask_for_data('Please, type yes or no: ').lower()
223
224
tandrii5d48c322016-08-18 16:19:37 -0700225def _git_branch_config_key(branch, key):
226 """Helper method to return Git config key for a branch."""
227 assert branch, 'branch name is required to set git config for it'
228 return 'branch.%s.%s' % (branch, key)
229
230
231def _git_get_branch_config_value(key, default=None, value_type=str,
232 branch=False):
233 """Returns git config value of given or current branch if any.
234
235 Returns default in all other cases.
236 """
237 assert value_type in (int, str, bool)
238 if branch is False: # Distinguishing default arg value from None.
239 branch = GetCurrentBranch()
240
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000241 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700242 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000243
tandrii5d48c322016-08-18 16:19:37 -0700244 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700245 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700246 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700247 # git config also has --int, but apparently git config suffers from integer
248 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700249 args.append(_git_branch_config_key(branch, key))
250 code, out = RunGitWithCode(args)
251 if code == 0:
252 value = out.strip()
253 if value_type == int:
254 return int(value)
255 if value_type == bool:
256 return bool(value.lower() == 'true')
257 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000258 return default
259
260
tandrii5d48c322016-08-18 16:19:37 -0700261def _git_set_branch_config_value(key, value, branch=None, **kwargs):
262 """Sets the value or unsets if it's None of a git branch config.
263
264 Valid, though not necessarily existing, branch must be provided,
265 otherwise currently checked out branch is used.
266 """
267 if not branch:
268 branch = GetCurrentBranch()
269 assert branch, 'a branch name OR currently checked out branch is required'
270 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700271 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700272 if value is None:
273 args.append('--unset')
274 elif isinstance(value, bool):
275 args.append('--bool')
276 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700277 else:
tandrii33a46ff2016-08-23 05:53:40 -0700278 # git config also has --int, but apparently git config suffers from integer
279 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700280 value = str(value)
281 args.append(_git_branch_config_key(branch, key))
282 if value is not None:
283 args.append(value)
284 RunGit(args, **kwargs)
285
286
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100287def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700288 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100289
290 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
291 """
292 # Git also stores timezone offset, but it only affects visual display,
293 # actual point in time is defined by this timestamp only.
294 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
295
296
297def _git_amend_head(message, committer_timestamp):
298 """Amends commit with new message and desired committer_timestamp.
299
300 Sets committer timezone to UTC.
301 """
302 env = os.environ.copy()
303 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
304 return RunGit(['commit', '--amend', '-m', message], env=env)
305
306
machenbach@chromium.org45453142015-09-15 08:45:22 +0000307def _get_properties_from_options(options):
308 properties = dict(x.split('=', 1) for x in options.properties)
309 for key, val in properties.iteritems():
310 try:
311 properties[key] = json.loads(val)
312 except ValueError:
313 pass # If a value couldn't be evaluated, treat it as a string.
314 return properties
315
316
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000317def _prefix_master(master):
318 """Convert user-specified master name to full master name.
319
320 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
321 name, while the developers always use shortened master name
322 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
323 function does the conversion for buildbucket migration.
324 """
borenet6c0efe62016-10-19 08:13:29 -0700325 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000326 return master
borenet6c0efe62016-10-19 08:13:29 -0700327 return '%s%s' % (MASTER_PREFIX, master)
328
329
330def _unprefix_master(bucket):
331 """Convert bucket name to shortened master name.
332
333 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
334 name, while the developers always use shortened master name
335 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
336 function does the conversion for buildbucket migration.
337 """
338 if bucket.startswith(MASTER_PREFIX):
339 return bucket[len(MASTER_PREFIX):]
340 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000341
342
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000343def _buildbucket_retry(operation_name, http, *args, **kwargs):
344 """Retries requests to buildbucket service and returns parsed json content."""
345 try_count = 0
346 while True:
347 response, content = http.request(*args, **kwargs)
348 try:
349 content_json = json.loads(content)
350 except ValueError:
351 content_json = None
352
353 # Buildbucket could return an error even if status==200.
354 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000355 error = content_json.get('error')
356 if error.get('code') == 403:
357 raise BuildbucketResponseException(
358 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000360 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000361 raise BuildbucketResponseException(msg)
362
363 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700364 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000365 raise BuildbucketResponseException(
366 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700367 'Please file bugs at http://crbug.com, '
368 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000369 content)
370 return content_json
371 if response.status < 500 or try_count >= 2:
372 raise httplib2.HttpLib2Error(content)
373
374 # status >= 500 means transient failures.
375 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700376 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000377 try_count += 1
378 assert False, 'unreachable'
379
380
qyearsley1fdfcb62016-10-24 13:22:03 -0700381def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700382 """Returns a dict mapping bucket names to builders and tests,
383 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700384 """
qyearsleydd49f942016-10-28 11:57:22 -0700385 # If no bots are listed, we try to get a set of builders and tests based
386 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700387 if not options.bot:
388 change = changelist.GetChange(
389 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700390 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700391 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700392 change=change,
393 changed_files=change.LocalPaths(),
394 repository_root=settings.GetRoot(),
395 default_presubmit=None,
396 project=None,
397 verbose=options.verbose,
398 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700399 if masters is None:
400 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100401 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700402
qyearsley1fdfcb62016-10-24 13:22:03 -0700403 if options.bucket:
404 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700405 if options.master:
406 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700407
qyearsleydd49f942016-10-28 11:57:22 -0700408 # If bots are listed but no master or bucket, then we need to find out
409 # the corresponding master for each bot.
410 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
411 if error_message:
412 option_parser.error(
413 'Tryserver master cannot be found because: %s\n'
414 'Please manually specify the tryserver master, e.g. '
415 '"-m tryserver.chromium.linux".' % error_message)
416 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700417
418
qyearsley123a4682016-10-26 09:12:17 -0700419def _get_bucket_map_for_builders(builders):
420 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700421 map_url = 'https://builders-map.appspot.com/'
422 try:
qyearsley123a4682016-10-26 09:12:17 -0700423 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700424 except urllib2.URLError as e:
425 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
426 (map_url, e))
427 except ValueError as e:
428 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700429 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700430 return None, 'Failed to build master map.'
431
qyearsley123a4682016-10-26 09:12:17 -0700432 bucket_map = {}
433 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800434 bucket = builders_map.get(builder, {}).get('bucket')
435 if bucket:
436 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700437 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700438
439
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800440def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700441 """Sends a request to Buildbucket to trigger try jobs for a changelist.
442
443 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700444 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700445 changelist: Changelist that the try jobs are associated with.
446 buckets: A nested dict mapping bucket names to builders to tests.
447 options: Command-line options.
448 """
tandriide281ae2016-10-12 06:02:30 -0700449 assert changelist.GetIssue(), 'CL must be uploaded first'
450 codereview_url = changelist.GetCodereviewServer()
451 assert codereview_url, 'CL must be uploaded first'
452 patchset = patchset or changelist.GetMostRecentPatchset()
453 assert patchset, 'CL must be uploaded first'
454
455 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700456 # Cache the buildbucket credentials under the codereview host key, so that
457 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700458 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459 http = authenticator.authorize(httplib2.Http())
460 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700461
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000462 buildbucket_put_url = (
463 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000464 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000465 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700466 hostname=codereview_host,
467 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000468 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700469
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700470 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800471 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700472 if options.clobber:
473 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700474 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700475 if extra_properties:
476 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000477
478 batch_req_body = {'builds': []}
479 print_text = []
480 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700481 for bucket, builders_and_tests in sorted(buckets.iteritems()):
482 print_text.append('Bucket: %s' % bucket)
483 master = None
484 if bucket.startswith(MASTER_PREFIX):
485 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000486 for builder, tests in sorted(builders_and_tests.iteritems()):
487 print_text.append(' %s: %s' % (builder, tests))
488 parameters = {
489 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000490 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100491 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000492 'revision': options.revision,
493 }],
tandrii8c5a3532016-11-04 07:52:02 -0700494 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000495 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000496 if 'presubmit' in builder.lower():
497 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000498 if tests:
499 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700500
501 tags = [
502 'builder:%s' % builder,
503 'buildset:%s' % buildset,
504 'user_agent:git_cl_try',
505 ]
506 if master:
507 parameters['properties']['master'] = master
508 tags.append('master:%s' % master)
509
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000510 batch_req_body['builds'].append(
511 {
512 'bucket': bucket,
513 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000514 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700515 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000516 }
517 )
518
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000519 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700520 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000521 http,
522 buildbucket_put_url,
523 'PUT',
524 body=json.dumps(batch_req_body),
525 headers={'Content-Type': 'application/json'}
526 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000527 print_text.append('To see results here, run: git cl try-results')
528 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700529 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000530
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000531
tandrii221ab252016-10-06 08:12:04 -0700532def fetch_try_jobs(auth_config, changelist, buildbucket_host,
533 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700534 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535
qyearsley53f48a12016-09-01 10:45:13 -0700536 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000537 """
tandrii221ab252016-10-06 08:12:04 -0700538 assert buildbucket_host
539 assert changelist.GetIssue(), 'CL must be uploaded first'
540 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
541 patchset = patchset or changelist.GetMostRecentPatchset()
542 assert patchset, 'CL must be uploaded first'
543
544 codereview_url = changelist.GetCodereviewServer()
545 codereview_host = urlparse.urlparse(codereview_url).hostname
546 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000547 if authenticator.has_cached_credentials():
548 http = authenticator.authorize(httplib2.Http())
549 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700550 print('Warning: Some results might be missing because %s' %
551 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700552 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 http = httplib2.Http()
554
555 http.force_exception_to_status_code = True
556
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000557 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700558 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700560 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000561 params = {'tag': 'buildset:%s' % buildset}
562
563 builds = {}
564 while True:
565 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700566 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700568 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 for build in content.get('builds', []):
570 builds[build['id']] = build
571 if 'next_cursor' in content:
572 params['start_cursor'] = content['next_cursor']
573 else:
574 break
575 return builds
576
577
qyearsleyeab3c042016-08-24 09:18:28 -0700578def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 """Prints nicely result of fetch_try_jobs."""
580 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700581 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000582 return
583
584 # Make a copy, because we'll be modifying builds dictionary.
585 builds = builds.copy()
586 builder_names_cache = {}
587
588 def get_builder(b):
589 try:
590 return builder_names_cache[b['id']]
591 except KeyError:
592 try:
593 parameters = json.loads(b['parameters_json'])
594 name = parameters['builder_name']
595 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700596 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700597 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000598 name = None
599 builder_names_cache[b['id']] = name
600 return name
601
602 def get_bucket(b):
603 bucket = b['bucket']
604 if bucket.startswith('master.'):
605 return bucket[len('master.'):]
606 return bucket
607
608 if options.print_master:
609 name_fmt = '%%-%ds %%-%ds' % (
610 max(len(str(get_bucket(b))) for b in builds.itervalues()),
611 max(len(str(get_builder(b))) for b in builds.itervalues()))
612 def get_name(b):
613 return name_fmt % (get_bucket(b), get_builder(b))
614 else:
615 name_fmt = '%%-%ds' % (
616 max(len(str(get_builder(b))) for b in builds.itervalues()))
617 def get_name(b):
618 return name_fmt % get_builder(b)
619
620 def sort_key(b):
621 return b['status'], b.get('result'), get_name(b), b.get('url')
622
623 def pop(title, f, color=None, **kwargs):
624 """Pop matching builds from `builds` dict and print them."""
625
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000626 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000627 colorize = str
628 else:
629 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
630
631 result = []
632 for b in builds.values():
633 if all(b.get(k) == v for k, v in kwargs.iteritems()):
634 builds.pop(b['id'])
635 result.append(b)
636 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700637 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000638 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700639 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000640
641 total = len(builds)
642 pop(status='COMPLETED', result='SUCCESS',
643 title='Successes:', color=Fore.GREEN,
644 f=lambda b: (get_name(b), b.get('url')))
645 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
646 title='Infra Failures:', color=Fore.MAGENTA,
647 f=lambda b: (get_name(b), b.get('url')))
648 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
649 title='Failures:', color=Fore.RED,
650 f=lambda b: (get_name(b), b.get('url')))
651 pop(status='COMPLETED', result='CANCELED',
652 title='Canceled:', color=Fore.MAGENTA,
653 f=lambda b: (get_name(b),))
654 pop(status='COMPLETED', result='FAILURE',
655 failure_reason='INVALID_BUILD_DEFINITION',
656 title='Wrong master/builder name:', color=Fore.MAGENTA,
657 f=lambda b: (get_name(b),))
658 pop(status='COMPLETED', result='FAILURE',
659 title='Other failures:',
660 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
661 pop(status='COMPLETED',
662 title='Other finished:',
663 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
664 pop(status='STARTED',
665 title='Started:', color=Fore.YELLOW,
666 f=lambda b: (get_name(b), b.get('url')))
667 pop(status='SCHEDULED',
668 title='Scheduled:',
669 f=lambda b: (get_name(b), 'id=%s' % b['id']))
670 # The last section is just in case buildbucket API changes OR there is a bug.
671 pop(title='Other:',
672 f=lambda b: (get_name(b), 'id=%s' % b['id']))
673 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700674 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000675
676
Aiden Bennerc08566e2018-10-03 17:52:42 +0000677def _ComputeDiffLineRanges(files, upstream_commit):
678 """Gets the changed line ranges for each file since upstream_commit.
679
680 Parses a git diff on provided files and returns a dict that maps a file name
681 to an ordered list of range tuples in the form (start_line, count).
682 Ranges are in the same format as a git diff.
683 """
684 # If files is empty then diff_output will be a full diff.
685 if len(files) == 0:
686 return {}
687
688 # Take diff and find the line ranges where there are changes.
689 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
690 diff_output = RunGit(diff_cmd)
691
692 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
693 # 2 capture groups
694 # 0 == fname of diff file
695 # 1 == 'diff_start,diff_count' or 'diff_start'
696 # will match each of
697 # diff --git a/foo.foo b/foo.py
698 # @@ -12,2 +14,3 @@
699 # @@ -12,2 +17 @@
700 # running re.findall on the above string with pattern will give
701 # [('foo.py', ''), ('', '14,3'), ('', '17')]
702
703 curr_file = None
704 line_diffs = {}
705 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
706 if match[0] != '':
707 # Will match the second filename in diff --git a/a.py b/b.py.
708 curr_file = match[0]
709 line_diffs[curr_file] = []
710 else:
711 # Matches +14,3
712 if ',' in match[1]:
713 diff_start, diff_count = match[1].split(',')
714 else:
715 # Single line changes are of the form +12 instead of +12,1.
716 diff_start = match[1]
717 diff_count = 1
718
719 diff_start = int(diff_start)
720 diff_count = int(diff_count)
721
722 # If diff_count == 0 this is a removal we can ignore.
723 line_diffs[curr_file].append((diff_start, diff_count))
724
725 return line_diffs
726
727
728def _FindYapfConfigFile(fpath,
729 yapf_config_cache,
730 top_dir=None,
731 default_style=None):
732 """Checks if a yapf file is in any parent directory of fpath until top_dir.
733
734 Recursively checks parent directories to find yapf file
735 and if no yapf file is found returns default_style.
736 Uses yapf_config_cache as a cache for previously found files.
737 """
738 # Return result if we've already computed it.
739 if fpath in yapf_config_cache:
740 return yapf_config_cache[fpath]
741
742 # Check if there is a style file in the current directory.
743 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
744 dirname = os.path.dirname(fpath)
745 if os.path.isfile(yapf_file):
746 ret = yapf_file
747 elif fpath == top_dir or dirname == fpath:
748 # If we're at the top level directory, or if we're at root
749 # use the chromium default yapf style.
750 ret = default_style
751 else:
752 # Otherwise recurse on the current directory.
753 ret = _FindYapfConfigFile(dirname, yapf_config_cache, top_dir,
754 default_style)
755 yapf_config_cache[fpath] = ret
756 return ret
757
758
qyearsley53f48a12016-09-01 10:45:13 -0700759def write_try_results_json(output_file, builds):
760 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
761
762 The input |builds| dict is assumed to be generated by Buildbucket.
763 Buildbucket documentation: http://goo.gl/G0s101
764 """
765
766 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800767 """Extracts some of the information from one build dict."""
768 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700769 return {
770 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700771 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800772 'builder_name': parameters.get('builder_name'),
773 'created_ts': build.get('created_ts'),
774 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700775 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800776 'result': build.get('result'),
777 'status': build.get('status'),
778 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700779 'url': build.get('url'),
780 }
781
782 converted = []
783 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000784 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700785 write_json(output_file, converted)
786
787
Aaron Gable13101a62018-02-09 13:20:41 -0800788def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000789 """Prints statistics about the change to the user."""
790 # --no-ext-diff is broken in some versions of Git, so try to work around
791 # this by overriding the environment (but there is still a problem if the
792 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000793 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000794 if 'GIT_EXTERNAL_DIFF' in env:
795 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000796
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000797 try:
798 stdout = sys.stdout.fileno()
799 except AttributeError:
800 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000801 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800802 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000803 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000804
805
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000806class BuildbucketResponseException(Exception):
807 pass
808
809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810class Settings(object):
811 def __init__(self):
812 self.default_server = None
813 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000814 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815 self.tree_status_url = None
816 self.viewvc_url = None
817 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000818 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000819 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000820 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000821 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000822 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000823 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824
825 def LazyUpdateIfNeeded(self):
826 """Updates the settings from a codereview.settings file, if available."""
827 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000828 # The only value that actually changes the behavior is
829 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000830 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000831 error_ok=True
832 ).strip().lower()
833
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000835 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 LoadCodereviewSettingsFromFile(cr_settings_file)
837 self.updated = True
838
839 def GetDefaultServerUrl(self, error_ok=False):
840 if not self.default_server:
841 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000842 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000843 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844 if error_ok:
845 return self.default_server
846 if not self.default_server:
847 error_message = ('Could not find settings file. You must configure '
848 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000849 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000850 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000851 return self.default_server
852
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000853 @staticmethod
854 def GetRelativeRoot():
855 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000856
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000858 if self.root is None:
859 self.root = os.path.abspath(self.GetRelativeRoot())
860 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000861
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000862 def GetGitMirror(self, remote='origin'):
863 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000864 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000865 if not os.path.isdir(local_url):
866 return None
867 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
868 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100869 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100870 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000871 if mirror.exists():
872 return mirror
873 return None
874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 def GetTreeStatusUrl(self, error_ok=False):
876 if not self.tree_status_url:
877 error_message = ('You must configure your tree status URL by running '
878 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000879 self.tree_status_url = self._GetRietveldConfig(
880 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881 return self.tree_status_url
882
883 def GetViewVCUrl(self):
884 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000885 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000886 return self.viewvc_url
887
rmistry@google.com90752582014-01-14 21:04:50 +0000888 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000889 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000890
rmistry@google.com78948ed2015-07-08 23:09:57 +0000891 def GetIsSkipDependencyUpload(self, branch_name):
892 """Returns true if specified branch should skip dep uploads."""
893 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
894 error_ok=True)
895
rmistry@google.com5626a922015-02-26 14:03:30 +0000896 def GetRunPostUploadHook(self):
897 run_post_upload_hook = self._GetRietveldConfig(
898 'run-post-upload-hook', error_ok=True)
899 return run_post_upload_hook == "True"
900
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000901 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000902 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000903
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000904 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000905 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000906
ukai@chromium.orge8077812012-02-03 03:41:46 +0000907 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700908 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000909 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700910 self.is_gerrit = (
911 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000912 return self.is_gerrit
913
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000914 def GetSquashGerritUploads(self):
915 """Return true if uploads to Gerrit should be squashed by default."""
916 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700917 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
918 if self.squash_gerrit_uploads is None:
919 # Default is squash now (http://crbug.com/611892#c23).
920 self.squash_gerrit_uploads = not (
921 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
922 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000923 return self.squash_gerrit_uploads
924
tandriia60502f2016-06-20 02:01:53 -0700925 def GetSquashGerritUploadsOverride(self):
926 """Return True or False if codereview.settings should be overridden.
927
928 Returns None if no override has been defined.
929 """
930 # See also http://crbug.com/611892#c23
931 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
932 error_ok=True).strip()
933 if result == 'true':
934 return True
935 if result == 'false':
936 return False
937 return None
938
tandrii@chromium.org28253532016-04-14 13:46:56 +0000939 def GetGerritSkipEnsureAuthenticated(self):
940 """Return True if EnsureAuthenticated should not be done for Gerrit
941 uploads."""
942 if self.gerrit_skip_ensure_authenticated is None:
943 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000944 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000945 error_ok=True).strip() == 'true')
946 return self.gerrit_skip_ensure_authenticated
947
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000948 def GetGitEditor(self):
949 """Return the editor specified in the git config, or None if none is."""
950 if self.git_editor is None:
951 self.git_editor = self._GetConfig('core.editor', error_ok=True)
952 return self.git_editor or None
953
thestig@chromium.org44202a22014-03-11 19:22:18 +0000954 def GetLintRegex(self):
955 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
956 DEFAULT_LINT_REGEX)
957
958 def GetLintIgnoreRegex(self):
959 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
960 DEFAULT_LINT_IGNORE_REGEX)
961
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000962 def GetProject(self):
963 if not self.project:
964 self.project = self._GetRietveldConfig('project', error_ok=True)
965 return self.project
966
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000967 def _GetRietveldConfig(self, param, **kwargs):
968 return self._GetConfig('rietveld.' + param, **kwargs)
969
rmistry@google.com78948ed2015-07-08 23:09:57 +0000970 def _GetBranchConfig(self, branch_name, param, **kwargs):
971 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
972
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000973 def _GetConfig(self, param, **kwargs):
974 self.LazyUpdateIfNeeded()
975 return RunGit(['config', param], **kwargs).strip()
976
977
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100978@contextlib.contextmanager
979def _get_gerrit_project_config_file(remote_url):
980 """Context manager to fetch and store Gerrit's project.config from
981 refs/meta/config branch and store it in temp file.
982
983 Provides a temporary filename or None if there was error.
984 """
985 error, _ = RunGitWithCode([
986 'fetch', remote_url,
987 '+refs/meta/config:refs/git_cl/meta/config'])
988 if error:
989 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700990 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100991 (remote_url, error))
992 yield None
993 return
994
995 error, project_config_data = RunGitWithCode(
996 ['show', 'refs/git_cl/meta/config:project.config'])
997 if error:
998 print('WARNING: project.config file not found')
999 yield None
1000 return
1001
1002 with gclient_utils.temporary_directory() as tempdir:
1003 project_config_file = os.path.join(tempdir, 'project.config')
1004 gclient_utils.FileWrite(project_config_file, project_config_data)
1005 yield project_config_file
1006
1007
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008def ShortBranchName(branch):
1009 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001010 return branch.replace('refs/heads/', '', 1)
1011
1012
1013def GetCurrentBranchRef():
1014 """Returns branch ref (e.g., refs/heads/master) or None."""
1015 return RunGit(['symbolic-ref', 'HEAD'],
1016 stderr=subprocess2.VOID, error_ok=True).strip() or None
1017
1018
1019def GetCurrentBranch():
1020 """Returns current branch or None.
1021
1022 For refs/heads/* branches, returns just last part. For others, full ref.
1023 """
1024 branchref = GetCurrentBranchRef()
1025 if branchref:
1026 return ShortBranchName(branchref)
1027 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001028
1029
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001030class _CQState(object):
1031 """Enum for states of CL with respect to Commit Queue."""
1032 NONE = 'none'
1033 DRY_RUN = 'dry_run'
1034 COMMIT = 'commit'
1035
1036 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1037
1038
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001039class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001040 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001041 self.issue = issue
1042 self.patchset = patchset
1043 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001044 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001045 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001046
1047 @property
1048 def valid(self):
1049 return self.issue is not None
1050
1051
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001052def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001053 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1054 fail_result = _ParsedIssueNumberArgument()
1055
1056 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001057 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001058 if not arg.startswith('http'):
1059 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001060
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001061 url = gclient_utils.UpgradeToHttps(arg)
1062 try:
1063 parsed_url = urlparse.urlparse(url)
1064 except ValueError:
1065 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001066
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001067 if codereview is not None:
1068 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1069 return parsed or fail_result
1070
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001071 results = {}
1072 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1073 parsed = cls.ParseIssueURL(parsed_url)
1074 if parsed is not None:
1075 results[name] = parsed
1076
1077 if not results:
1078 return fail_result
1079 if len(results) == 1:
1080 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001081
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001082 return results['gerrit']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001083
1084
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001085def _create_description_from_log(args):
1086 """Pulls out the commit log to use as a base for the CL description."""
1087 log_args = []
1088 if len(args) == 1 and not args[0].endswith('.'):
1089 log_args = [args[0] + '..']
1090 elif len(args) == 1 and args[0].endswith('...'):
1091 log_args = [args[0][:-1]]
1092 elif len(args) == 2:
1093 log_args = [args[0] + '..' + args[1]]
1094 else:
1095 log_args = args[:] # Hope for the best!
1096 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1097
1098
Aaron Gablea45ee112016-11-22 15:14:38 -08001099class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001100 def __init__(self, issue, url):
1101 self.issue = issue
1102 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001103 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001104
1105 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001106 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001107 self.issue, self.url)
1108
1109
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001110_CommentSummary = collections.namedtuple(
1111 '_CommentSummary', ['date', 'message', 'sender',
1112 # TODO(tandrii): these two aren't known in Gerrit.
1113 'approval', 'disapproval'])
1114
1115
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001117 """Changelist works with one changelist in local branch.
1118
1119 Supports two codereview backends: Rietveld or Gerrit, selected at object
1120 creation.
1121
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001122 Notes:
1123 * Not safe for concurrent multi-{thread,process} use.
1124 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001125 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001126 """
1127
1128 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1129 """Create a new ChangeList instance.
1130
1131 If issue is given, the codereview must be given too.
1132
1133 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1134 Otherwise, it's decided based on current configuration of the local branch,
1135 with default being 'rietveld' for backwards compatibility.
1136 See _load_codereview_impl for more details.
1137
1138 **kwargs will be passed directly to codereview implementation.
1139 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001141 global settings
1142 if not settings:
1143 # Happens when git_cl.py is used as a utility library.
1144 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001145
1146 if issue:
1147 assert codereview, 'codereview must be known, if issue is known'
1148
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 self.branchref = branchref
1150 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001151 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152 self.branch = ShortBranchName(self.branchref)
1153 else:
1154 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001156 self.lookedup_issue = False
1157 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 self.has_description = False
1159 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001160 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001162 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001163 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001164 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001165 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001166
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001167 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001168 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001170 assert self._codereview_impl
1171 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001172
1173 def _load_codereview_impl(self, codereview=None, **kwargs):
1174 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001175 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1176 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1177 self._codereview = codereview
1178 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001179 return
1180
1181 # Automatic selection based on issue number set for a current branch.
1182 # Rietveld takes precedence over Gerrit.
1183 assert not self.issue
1184 # Whether we find issue or not, we are doing the lookup.
1185 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001186 if self.GetBranch():
1187 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1188 issue = _git_get_branch_config_value(
1189 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1190 if issue:
1191 self._codereview = codereview
1192 self._codereview_impl = cls(self, **kwargs)
1193 self.issue = int(issue)
1194 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001195
1196 # No issue is set for this branch, so decide based on repo-wide settings.
1197 return self._load_codereview_impl(
1198 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1199 **kwargs)
1200
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001201 def IsGerrit(self):
1202 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001203
1204 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001205 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001206
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001207 The return value is a string suitable for passing to git cl with the --cc
1208 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001209 """
1210 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001211 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001212 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001213 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1214 return self.cc
1215
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001216 def GetCCListWithoutDefault(self):
1217 """Return the users cc'd on this CL excluding default ones."""
1218 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001219 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001220 return self.cc
1221
Daniel Cheng7227d212017-11-17 08:12:37 -08001222 def ExtendCC(self, more_cc):
1223 """Extends the list of users to cc on this CL based on the changed files."""
1224 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225
1226 def GetBranch(self):
1227 """Returns the short branch name, e.g. 'master'."""
1228 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001229 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001230 if not branchref:
1231 return None
1232 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 self.branch = ShortBranchName(self.branchref)
1234 return self.branch
1235
1236 def GetBranchRef(self):
1237 """Returns the full branch name, e.g. 'refs/heads/master'."""
1238 self.GetBranch() # Poke the lazy loader.
1239 return self.branchref
1240
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001241 def ClearBranch(self):
1242 """Clears cached branch data of this object."""
1243 self.branch = self.branchref = None
1244
tandrii5d48c322016-08-18 16:19:37 -07001245 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1246 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1247 kwargs['branch'] = self.GetBranch()
1248 return _git_get_branch_config_value(key, default, **kwargs)
1249
1250 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1251 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1252 assert self.GetBranch(), (
1253 'this CL must have an associated branch to %sset %s%s' %
1254 ('un' if value is None else '',
1255 key,
1256 '' if value is None else ' to %r' % value))
1257 kwargs['branch'] = self.GetBranch()
1258 return _git_set_branch_config_value(key, value, **kwargs)
1259
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001260 @staticmethod
1261 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001262 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263 e.g. 'origin', 'refs/heads/master'
1264 """
1265 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001266 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1267
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001269 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001271 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1272 error_ok=True).strip()
1273 if upstream_branch:
1274 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001276 # Else, try to guess the origin remote.
1277 remote_branches = RunGit(['branch', '-r']).split()
1278 if 'origin/master' in remote_branches:
1279 # Fall back on origin/master if it exits.
1280 remote = 'origin'
1281 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001283 DieWithError(
1284 'Unable to determine default branch to diff against.\n'
1285 'Either pass complete "git diff"-style arguments, like\n'
1286 ' git cl upload origin/master\n'
1287 'or verify this branch is set up to track another \n'
1288 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001289
1290 return remote, upstream_branch
1291
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001292 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001293 upstream_branch = self.GetUpstreamBranch()
1294 if not BranchExists(upstream_branch):
1295 DieWithError('The upstream for the current branch (%s) does not exist '
1296 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001297 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001298 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001299
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 def GetUpstreamBranch(self):
1301 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001302 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001304 upstream_branch = upstream_branch.replace('refs/heads/',
1305 'refs/remotes/%s/' % remote)
1306 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1307 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308 self.upstream_branch = upstream_branch
1309 return self.upstream_branch
1310
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001311 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001312 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001313 remote, branch = None, self.GetBranch()
1314 seen_branches = set()
1315 while branch not in seen_branches:
1316 seen_branches.add(branch)
1317 remote, branch = self.FetchUpstreamTuple(branch)
1318 branch = ShortBranchName(branch)
1319 if remote != '.' or branch.startswith('refs/remotes'):
1320 break
1321 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001322 remotes = RunGit(['remote'], error_ok=True).split()
1323 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001324 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001325 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001326 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001327 logging.warn('Could not determine which remote this change is '
1328 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001329 else:
1330 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001331 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001332 branch = 'HEAD'
1333 if branch.startswith('refs/remotes'):
1334 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001335 elif branch.startswith('refs/branch-heads/'):
1336 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001337 else:
1338 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001339 return self._remote
1340
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001341 def GitSanityChecks(self, upstream_git_obj):
1342 """Checks git repo status and ensures diff is from local commits."""
1343
sbc@chromium.org79706062015-01-14 21:18:12 +00001344 if upstream_git_obj is None:
1345 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001346 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001347 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001348 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001349 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001350 return False
1351
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001352 # Verify the commit we're diffing against is in our current branch.
1353 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1354 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1355 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001356 print('ERROR: %s is not in the current branch. You may need to rebase '
1357 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001358 return False
1359
1360 # List the commits inside the diff, and verify they are all local.
1361 commits_in_diff = RunGit(
1362 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1363 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1364 remote_branch = remote_branch.strip()
1365 if code != 0:
1366 _, remote_branch = self.GetRemoteBranch()
1367
1368 commits_in_remote = RunGit(
1369 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1370
1371 common_commits = set(commits_in_diff) & set(commits_in_remote)
1372 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001373 print('ERROR: Your diff contains %d commits already in %s.\n'
1374 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1375 'the diff. If you are using a custom git flow, you can override'
1376 ' the reference used for this check with "git config '
1377 'gitcl.remotebranch <git-ref>".' % (
1378 len(common_commits), remote_branch, upstream_git_obj),
1379 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001380 return False
1381 return True
1382
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001383 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001384 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001385
1386 Returns None if it is not set.
1387 """
tandrii5d48c322016-08-18 16:19:37 -07001388 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001389
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390 def GetRemoteUrl(self):
1391 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1392
1393 Returns None if there is no remote.
1394 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001395 is_cached, value = self._cached_remote_url
1396 if is_cached:
1397 return value
1398
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001399 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001400 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1401
1402 # If URL is pointing to a local directory, it is probably a git cache.
1403 if os.path.isdir(url):
1404 url = RunGit(['config', 'remote.%s.url' % remote],
1405 error_ok=True,
1406 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001407 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001408 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001410 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001411 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001412 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001413 self.issue = self._GitGetBranchConfigValue(
1414 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001415 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 return self.issue
1417
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418 def GetIssueURL(self):
1419 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001420 issue = self.GetIssue()
1421 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001422 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001423 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001424
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001425 def GetDescription(self, pretty=False, force=False):
1426 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001428 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 self.has_description = True
1430 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001431 # Set width to 72 columns + 2 space indent.
1432 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001434 lines = self.description.splitlines()
1435 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436 return self.description
1437
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001438 def GetDescriptionFooters(self):
1439 """Returns (non_footer_lines, footers) for the commit message.
1440
1441 Returns:
1442 non_footer_lines (list(str)) - Simple list of description lines without
1443 any footer. The lines do not contain newlines, nor does the list contain
1444 the empty line between the message and the footers.
1445 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1446 [("Change-Id", "Ideadbeef...."), ...]
1447 """
1448 raw_description = self.GetDescription()
1449 msg_lines, _, footers = git_footers.split_footers(raw_description)
1450 if footers:
1451 msg_lines = msg_lines[:len(msg_lines)-1]
1452 return msg_lines, footers
1453
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001455 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001456 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001457 self.patchset = self._GitGetBranchConfigValue(
1458 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001459 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001460 return self.patchset
1461
1462 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001463 """Set this branch's patchset. If patchset=0, clears the patchset."""
1464 assert self.GetBranch()
1465 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001466 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001467 else:
1468 self.patchset = int(patchset)
1469 self._GitSetBranchConfigValue(
1470 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001471
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001472 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001473 """Set this branch's issue. If issue isn't given, clears the issue."""
1474 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001476 issue = int(issue)
1477 self._GitSetBranchConfigValue(
1478 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001479 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001480 codereview_server = self._codereview_impl.GetCodereviewServer()
1481 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001482 self._GitSetBranchConfigValue(
1483 self._codereview_impl.CodereviewServerConfigKey(),
1484 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001485 else:
tandrii5d48c322016-08-18 16:19:37 -07001486 # Reset all of these just to be clean.
1487 reset_suffixes = [
1488 'last-upload-hash',
1489 self._codereview_impl.IssueConfigKey(),
1490 self._codereview_impl.PatchsetConfigKey(),
1491 self._codereview_impl.CodereviewServerConfigKey(),
1492 ] + self._PostUnsetIssueProperties()
1493 for prop in reset_suffixes:
1494 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001495 msg = RunGit(['log', '-1', '--format=%B']).strip()
1496 if msg and git_footers.get_footer_change_id(msg):
1497 print('WARNING: The change patched into this branch has a Change-Id. '
1498 'Removing it.')
1499 RunGit(['commit', '--amend', '-m',
1500 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001501 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001502 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001503
dnjba1b0f32016-09-02 12:37:42 -07001504 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001505 if not self.GitSanityChecks(upstream_branch):
1506 DieWithError('\nGit sanity check failure')
1507
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001508 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001509 if not root:
1510 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001511 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001512
1513 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001514 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001515 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001516 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001517 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001518 except subprocess2.CalledProcessError:
1519 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001520 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001521 'This branch probably doesn\'t exist anymore. To reset the\n'
1522 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001523 ' git branch --set-upstream-to origin/master %s\n'
1524 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001525 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001526
maruel@chromium.org52424302012-08-29 15:14:30 +00001527 issue = self.GetIssue()
1528 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001529 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001530 description = self.GetDescription()
1531 else:
1532 # If the change was never uploaded, use the log messages of all commits
1533 # up to the branch point, as git cl upload will prefill the description
1534 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001535 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1536 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001537
1538 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001539 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001540 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001541 name,
1542 description,
1543 absroot,
1544 files,
1545 issue,
1546 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001547 author,
1548 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001549
dsansomee2d6fd92016-09-08 00:10:47 -07001550 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001551 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001552 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001553 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001554
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001555 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1556 """Sets the description for this CL remotely.
1557
1558 You can get description_lines and footers with GetDescriptionFooters.
1559
1560 Args:
1561 description_lines (list(str)) - List of CL description lines without
1562 newline characters.
1563 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1564 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1565 `List-Of-Tokens`). It will be case-normalized so that each token is
1566 title-cased.
1567 """
1568 new_description = '\n'.join(description_lines)
1569 if footers:
1570 new_description += '\n'
1571 for k, v in footers:
1572 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1573 if not git_footers.FOOTER_PATTERN.match(foot):
1574 raise ValueError('Invalid footer %r' % foot)
1575 new_description += foot + '\n'
1576 self.UpdateDescription(new_description, force)
1577
Edward Lesmes8e282792018-04-03 18:50:29 -04001578 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001579 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1580 try:
1581 return presubmit_support.DoPresubmitChecks(change, committing,
1582 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1583 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001584 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1585 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001586 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001587 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001588
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001589 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1590 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001591 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1592 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001593 else:
1594 # Assume url.
1595 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1596 urlparse.urlparse(issue_arg))
1597 if not parsed_issue_arg or not parsed_issue_arg.valid:
1598 DieWithError('Failed to parse issue argument "%s". '
1599 'Must be an issue number or a valid URL.' % issue_arg)
1600 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001601 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001602
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001603 def CMDUpload(self, options, git_diff_args, orig_args):
1604 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001605 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001606 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001607 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001608 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001609 else:
1610 if self.GetBranch() is None:
1611 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1612
1613 # Default to diffing against common ancestor of upstream branch
1614 base_branch = self.GetCommonAncestorWithUpstream()
1615 git_diff_args = [base_branch, 'HEAD']
1616
Aaron Gablec4c40d12017-05-22 11:49:53 -07001617
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001618 # Fast best-effort checks to abort before running potentially
1619 # expensive hooks if uploading is likely to fail anyway. Passing these
1620 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001621 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001622 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001623
1624 # Apply watchlists on upload.
1625 change = self.GetChange(base_branch, None)
1626 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1627 files = [f.LocalPath() for f in change.AffectedFiles()]
1628 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001629 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001630
1631 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001632 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001633 # Set the reviewer list now so that presubmit checks can access it.
1634 change_description = ChangeDescription(change.FullDescriptionText())
1635 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001636 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001637 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001638 change)
1639 change.SetDescriptionText(change_description.description)
1640 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001641 may_prompt=not options.force,
1642 verbose=options.verbose,
1643 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001644 if not hook_results.should_continue():
1645 return 1
1646 if not options.reviewers and hook_results.reviewers:
1647 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001648 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001649
Aaron Gable13101a62018-02-09 13:20:41 -08001650 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001651 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001652 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001653 _git_set_branch_config_value('last-upload-hash',
1654 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001655 # Run post upload hooks, if specified.
1656 if settings.GetRunPostUploadHook():
1657 presubmit_support.DoPostUploadExecuter(
1658 change,
1659 self,
1660 settings.GetRoot(),
1661 options.verbose,
1662 sys.stdout)
1663
1664 # Upload all dependencies if specified.
1665 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001666 print()
1667 print('--dependencies has been specified.')
1668 print('All dependent local branches will be re-uploaded.')
1669 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001670 # Remove the dependencies flag from args so that we do not end up in a
1671 # loop.
1672 orig_args.remove('--dependencies')
1673 ret = upload_branch_deps(self, orig_args)
1674 return ret
1675
Ravi Mistry31e7d562018-04-02 12:53:57 -04001676 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1677 """Sets labels on the change based on the provided flags.
1678
1679 Sets labels if issue is already uploaded and known, else returns without
1680 doing anything.
1681
1682 Args:
1683 enable_auto_submit: Sets Auto-Submit+1 on the change.
1684 use_commit_queue: Sets Commit-Queue+2 on the change.
1685 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1686 both use_commit_queue and cq_dry_run are true.
1687 """
1688 if not self.GetIssue():
1689 return
1690 try:
1691 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1692 cq_dry_run)
1693 return 0
1694 except KeyboardInterrupt:
1695 raise
1696 except:
1697 labels = []
1698 if enable_auto_submit:
1699 labels.append('Auto-Submit')
1700 if use_commit_queue or cq_dry_run:
1701 labels.append('Commit-Queue')
1702 print('WARNING: Failed to set label(s) on your change: %s\n'
1703 'Either:\n'
1704 ' * Your project does not have the above label(s),\n'
1705 ' * You don\'t have permission to set the above label(s),\n'
1706 ' * There\'s a bug in this code (see stack trace below).\n' %
1707 (', '.join(labels)))
1708 # Still raise exception so that stack trace is printed.
1709 raise
1710
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001711 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001712 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001713
1714 Issue must have been already uploaded and known.
1715 """
1716 assert new_state in _CQState.ALL_STATES
1717 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001718 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001719 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001720 return 0
1721 except KeyboardInterrupt:
1722 raise
1723 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001724 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001725 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001726 ' * Your project has no CQ,\n'
1727 ' * You don\'t have permission to change the CQ state,\n'
1728 ' * There\'s a bug in this code (see stack trace below).\n'
1729 'Consider specifying which bots to trigger manually or asking your '
1730 'project owners for permissions or contacting Chrome Infra at:\n'
1731 'https://www.chromium.org/infra\n\n' %
1732 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001733 # Still raise exception so that stack trace is printed.
1734 raise
1735
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001736 # Forward methods to codereview specific implementation.
1737
Aaron Gable636b13f2017-07-14 10:42:48 -07001738 def AddComment(self, message, publish=None):
1739 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001740
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001741 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001742 """Returns list of _CommentSummary for each comment.
1743
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001744 args:
1745 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001746 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001747 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001748
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001749 def CloseIssue(self):
1750 return self._codereview_impl.CloseIssue()
1751
1752 def GetStatus(self):
1753 return self._codereview_impl.GetStatus()
1754
1755 def GetCodereviewServer(self):
1756 return self._codereview_impl.GetCodereviewServer()
1757
tandriide281ae2016-10-12 06:02:30 -07001758 def GetIssueOwner(self):
1759 """Get owner from codereview, which may differ from this checkout."""
1760 return self._codereview_impl.GetIssueOwner()
1761
Edward Lemur707d70b2018-02-07 00:50:14 +01001762 def GetReviewers(self):
1763 return self._codereview_impl.GetReviewers()
1764
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001765 def GetMostRecentPatchset(self):
1766 return self._codereview_impl.GetMostRecentPatchset()
1767
tandriide281ae2016-10-12 06:02:30 -07001768 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001769 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001770 return self._codereview_impl.CannotTriggerTryJobReason()
1771
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001772 def GetTryJobProperties(self, patchset=None):
1773 """Returns dictionary of properties to launch try job."""
1774 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001775
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001776 def __getattr__(self, attr):
1777 # This is because lots of untested code accesses Rietveld-specific stuff
1778 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001779 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001780 # Note that child method defines __getattr__ as well, and forwards it here,
1781 # because _RietveldChangelistImpl is not cleaned up yet, and given
1782 # deprecation of Rietveld, it should probably be just removed.
1783 # Until that time, avoid infinite recursion by bypassing __getattr__
1784 # of implementation class.
1785 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001786
1787
1788class _ChangelistCodereviewBase(object):
1789 """Abstract base class encapsulating codereview specifics of a changelist."""
1790 def __init__(self, changelist):
1791 self._changelist = changelist # instance of Changelist
1792
1793 def __getattr__(self, attr):
1794 # Forward methods to changelist.
1795 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1796 # _RietveldChangelistImpl to avoid this hack?
1797 return getattr(self._changelist, attr)
1798
1799 def GetStatus(self):
1800 """Apply a rough heuristic to give a simple summary of an issue's review
1801 or CQ status, assuming adherence to a common workflow.
1802
1803 Returns None if no issue for this branch, or specific string keywords.
1804 """
1805 raise NotImplementedError()
1806
1807 def GetCodereviewServer(self):
1808 """Returns server URL without end slash, like "https://codereview.com"."""
1809 raise NotImplementedError()
1810
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001811 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001812 """Fetches and returns description from the codereview server."""
1813 raise NotImplementedError()
1814
tandrii5d48c322016-08-18 16:19:37 -07001815 @classmethod
1816 def IssueConfigKey(cls):
1817 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001818 raise NotImplementedError()
1819
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001820 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001821 def PatchsetConfigKey(cls):
1822 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001823 raise NotImplementedError()
1824
tandrii5d48c322016-08-18 16:19:37 -07001825 @classmethod
1826 def CodereviewServerConfigKey(cls):
1827 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001828 raise NotImplementedError()
1829
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001830 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001831 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001832 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001833
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001834 def GetGerritObjForPresubmit(self):
1835 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1836 return None
1837
dsansomee2d6fd92016-09-08 00:10:47 -07001838 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001839 """Update the description on codereview site."""
1840 raise NotImplementedError()
1841
Aaron Gable636b13f2017-07-14 10:42:48 -07001842 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001843 """Posts a comment to the codereview site."""
1844 raise NotImplementedError()
1845
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001846 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001847 raise NotImplementedError()
1848
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001849 def CloseIssue(self):
1850 """Closes the issue."""
1851 raise NotImplementedError()
1852
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001853 def GetMostRecentPatchset(self):
1854 """Returns the most recent patchset number from the codereview site."""
1855 raise NotImplementedError()
1856
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001857 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001858 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001859 """Fetches and applies the issue.
1860
1861 Arguments:
1862 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1863 reject: if True, reject the failed patch instead of switching to 3-way
1864 merge. Rietveld only.
1865 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1866 only.
1867 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001868 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001869 """
1870 raise NotImplementedError()
1871
1872 @staticmethod
1873 def ParseIssueURL(parsed_url):
1874 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1875 failed."""
1876 raise NotImplementedError()
1877
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001878 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001879 """Best effort check that user is authenticated with codereview server.
1880
1881 Arguments:
1882 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001883 refresh: whether to attempt to refresh credentials. Ignored if not
1884 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001885 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001886 raise NotImplementedError()
1887
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001888 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001889 """Best effort check that uploading isn't supposed to fail for predictable
1890 reasons.
1891
1892 This method should raise informative exception if uploading shouldn't
1893 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001894
1895 Arguments:
1896 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001897 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001898 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001899
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001900 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001901 """Uploads a change to codereview."""
1902 raise NotImplementedError()
1903
Ravi Mistry31e7d562018-04-02 12:53:57 -04001904 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1905 """Sets labels on the change based on the provided flags.
1906
1907 Issue must have been already uploaded and known.
1908 """
1909 raise NotImplementedError()
1910
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001911 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001912 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001913
1914 Issue must have been already uploaded and known.
1915 """
1916 raise NotImplementedError()
1917
tandriie113dfd2016-10-11 10:20:12 -07001918 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001919 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001920 raise NotImplementedError()
1921
tandriide281ae2016-10-12 06:02:30 -07001922 def GetIssueOwner(self):
1923 raise NotImplementedError()
1924
Edward Lemur707d70b2018-02-07 00:50:14 +01001925 def GetReviewers(self):
1926 raise NotImplementedError()
1927
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001928 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001929 raise NotImplementedError()
1930
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001931
1932class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001933
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001934 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001935 super(_RietveldChangelistImpl, self).__init__(changelist)
1936 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001937 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001938 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001939
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001940 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001941 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001942 self._props = None
1943 self._rpc_server = None
1944
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001945 def GetCodereviewServer(self):
1946 if not self._rietveld_server:
1947 # If we're on a branch then get the server potentially associated
1948 # with that branch.
1949 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001950 self._rietveld_server = gclient_utils.UpgradeToHttps(
1951 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001952 if not self._rietveld_server:
1953 self._rietveld_server = settings.GetDefaultServerUrl()
1954 return self._rietveld_server
1955
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001956 def EnsureAuthenticated(self, force, refresh=False):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001957 # No checks for Rietveld because we are deprecating Rietveld.
1958 pass
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001959
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001960 def EnsureCanUploadPatchset(self, force):
1961 # No checks for Rietveld because we are deprecating Rietveld.
1962 pass
1963
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001964 def FetchDescription(self, force=False):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001965 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001966
1967 def GetMostRecentPatchset(self):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001968 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001969
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001970 def GetIssueProperties(self):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001971 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001972
tandriie113dfd2016-10-11 10:20:12 -07001973 def CannotTriggerTryJobReason(self):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00001974 raise NotImplementedError()
tandriie113dfd2016-10-11 10:20:12 -07001975
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001976 def GetTryJobProperties(self, patchset=None):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00001977 raise NotImplementedError()
tandrii8c5a3532016-11-04 07:52:02 -07001978
tandriide281ae2016-10-12 06:02:30 -07001979 def GetIssueOwner(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001980 raise NotImplementedError()
tandriide281ae2016-10-12 06:02:30 -07001981
Edward Lemur707d70b2018-02-07 00:50:14 +01001982 def GetReviewers(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001983 raise NotImplementedError()
Edward Lemur707d70b2018-02-07 00:50:14 +01001984
Aaron Gable636b13f2017-07-14 10:42:48 -07001985 def AddComment(self, message, publish=None):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001986 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001987
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001988 def GetCommentsSummary(self, readable=True):
1989 raise NotImplementedError()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001990
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001991 def GetStatus(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001992 print(
1993 'WARNING! Rietveld is no longer supported.\n'
1994 '\n'
1995 'If you have old branches in your checkout, please archive/delete them.\n'
1996 ' $ git cl archive --help\n'
1997 '\n'
1998 'See also PSA https://groups.google.com/a/chromium.org/'
1999 'forum/#!topic/infra-dev/2DIVzM2wseo\n')
2000 return 'rietveld-not-supported'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002001
dsansomee2d6fd92016-09-08 00:10:47 -07002002 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00002003 raise NotImplementedError()
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002005 def CloseIssue(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00002006 raise NotImplementedError()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002007
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002008 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002009 return self.SetFlags({flag: value})
2010
2011 def SetFlags(self, flags):
2012 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002013 """
phajdan.jr68598232016-08-10 03:28:28 -07002014 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002015 try:
tandrii4b233bd2016-07-06 03:50:29 -07002016 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002017 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002018 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002019 if e.code == 404:
2020 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2021 if e.code == 403:
2022 DieWithError(
2023 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002024 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002025 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002027 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028 """Returns an upload.RpcServer() to access this review's rietveld instance.
2029 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002030 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002031 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002032 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002033 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002034 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002035
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002036 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002037 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002038 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002039
tandrii5d48c322016-08-18 16:19:37 -07002040 @classmethod
2041 def PatchsetConfigKey(cls):
2042 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043
tandrii5d48c322016-08-18 16:19:37 -07002044 @classmethod
2045 def CodereviewServerConfigKey(cls):
2046 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002047
Ravi Mistry31e7d562018-04-02 12:53:57 -04002048 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2049 raise NotImplementedError()
2050
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002051 def SetCQState(self, new_state):
2052 props = self.GetIssueProperties()
2053 if props.get('private'):
2054 DieWithError('Cannot set-commit on private issue')
2055
2056 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002057 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002058 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002059 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002060 else:
tandrii4b233bd2016-07-06 03:50:29 -07002061 assert new_state == _CQState.DRY_RUN
2062 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002063
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002064 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002065 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002066 # PatchIssue should never be called with a dirty tree. It is up to the
2067 # caller to check this, but just in case we assert here since the
2068 # consequences of the caller not checking this could be dire.
2069 assert(not git_common.is_dirty_git_tree('apply'))
2070 assert(parsed_issue_arg.valid)
2071 self._changelist.issue = parsed_issue_arg.issue
2072 if parsed_issue_arg.hostname:
2073 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2074
skobes6468b902016-10-24 08:45:10 -07002075 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2076 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2077 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002078 try:
skobes6468b902016-10-24 08:45:10 -07002079 scm_obj.apply_patch(patchset_object)
2080 except Exception as e:
2081 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002082 return 1
2083
2084 # If we had an issue, commit the current state and register the issue.
2085 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002086 self.SetIssue(self.GetIssue())
2087 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002088 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2089 'patch from issue %(i)s at patchset '
2090 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2091 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002092 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002093 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002094 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002095 return 0
2096
2097 @staticmethod
2098 def ParseIssueURL(parsed_url):
2099 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2100 return None
wychen3c1c1722016-08-04 11:46:36 -07002101 # Rietveld patch: https://domain/<number>/#ps<patchset>
2102 match = re.match(r'/(\d+)/$', parsed_url.path)
2103 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2104 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002105 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002106 issue=int(match.group(1)),
2107 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002108 hostname=parsed_url.netloc,
2109 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002110 # Typical url: https://domain/<issue_number>[/[other]]
2111 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2112 if match:
skobes6468b902016-10-24 08:45:10 -07002113 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002114 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002115 hostname=parsed_url.netloc,
2116 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002117 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2118 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2119 if match:
skobes6468b902016-10-24 08:45:10 -07002120 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002121 issue=int(match.group(1)),
2122 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002123 hostname=parsed_url.netloc,
2124 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002125 return None
2126
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002127 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002128 """Upload the patch to Rietveld."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00002129 raise NotImplementedError
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002130
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002131
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002132class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002133 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002134 # auth_config is Rietveld thing, kept here to preserve interface only.
2135 super(_GerritChangelistImpl, self).__init__(changelist)
2136 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002137 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002138 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002139 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002140 # Map from change number (issue) to its detail cache.
2141 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002142
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002143 if codereview_host is not None:
2144 assert not codereview_host.startswith('https://'), codereview_host
2145 self._gerrit_host = codereview_host
2146 self._gerrit_server = 'https://%s' % codereview_host
2147
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002148 def _GetGerritHost(self):
2149 # Lazy load of configs.
2150 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002151 if self._gerrit_host and '.' not in self._gerrit_host:
2152 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2153 # This happens for internal stuff http://crbug.com/614312.
2154 parsed = urlparse.urlparse(self.GetRemoteUrl())
2155 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002156 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002157 ' Your current remote is: %s' % self.GetRemoteUrl())
2158 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2159 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002160 return self._gerrit_host
2161
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002162 def _GetGitHost(self):
2163 """Returns git host to be used when uploading change to Gerrit."""
2164 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2165
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002166 def GetCodereviewServer(self):
2167 if not self._gerrit_server:
2168 # If we're on a branch then get the server potentially associated
2169 # with that branch.
2170 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002171 self._gerrit_server = self._GitGetBranchConfigValue(
2172 self.CodereviewServerConfigKey())
2173 if self._gerrit_server:
2174 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002175 if not self._gerrit_server:
2176 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2177 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002178 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002179 parts[0] = parts[0] + '-review'
2180 self._gerrit_host = '.'.join(parts)
2181 self._gerrit_server = 'https://%s' % self._gerrit_host
2182 return self._gerrit_server
2183
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002184 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002185 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002186 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002187 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002188 logging.warn('can\'t detect Gerrit project.')
2189 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002190 project = urlparse.urlparse(remote_url).path.strip('/')
2191 if project.endswith('.git'):
2192 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00002193 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
2194 # 'a/' prefix, because 'a/' prefix is used to force authentication in
2195 # gitiles/git-over-https protocol. E.g.,
2196 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
2197 # as
2198 # https://chromium.googlesource.com/v8/v8
2199 if project.startswith('a/'):
2200 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002201 return project
2202
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002203 def _GerritChangeIdentifier(self):
2204 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2205
2206 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002207 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002208 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002209 project = self._GetGerritProject()
2210 if project:
2211 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2212 # Fall back on still unique, but less efficient change number.
2213 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002214
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002215 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002216 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002217 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002218
tandrii5d48c322016-08-18 16:19:37 -07002219 @classmethod
2220 def PatchsetConfigKey(cls):
2221 return 'gerritpatchset'
2222
2223 @classmethod
2224 def CodereviewServerConfigKey(cls):
2225 return 'gerritserver'
2226
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002227 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002228 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002229 if settings.GetGerritSkipEnsureAuthenticated():
2230 # For projects with unusual authentication schemes.
2231 # See http://crbug.com/603378.
2232 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002233
2234 # Check presence of cookies only if using cookies-based auth method.
2235 cookie_auth = gerrit_util.Authenticator.get()
2236 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002237 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002238
2239 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002240 self.GetCodereviewServer()
2241 git_host = self._GetGitHost()
2242 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002243
2244 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2245 git_auth = cookie_auth.get_auth_header(git_host)
2246 if gerrit_auth and git_auth:
2247 if gerrit_auth == git_auth:
2248 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002249 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002250 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002251 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002252 ' %s\n'
2253 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002254 ' Consider running the following command:\n'
2255 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002256 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002257 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002258 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002259 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002260 cookie_auth.get_new_password_message(git_host)))
2261 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002262 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002263 return
2264 else:
2265 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002266 ([] if gerrit_auth else [self._gerrit_host]) +
2267 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002268 DieWithError('Credentials for the following hosts are required:\n'
2269 ' %s\n'
2270 'These are read from %s (or legacy %s)\n'
2271 '%s' % (
2272 '\n '.join(missing),
2273 cookie_auth.get_gitcookies_path(),
2274 cookie_auth.get_netrc_path(),
2275 cookie_auth.get_new_password_message(git_host)))
2276
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002277 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002278 if not self.GetIssue():
2279 return
2280
2281 # Warm change details cache now to avoid RPCs later, reducing latency for
2282 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002283 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002284 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002285
2286 status = self._GetChangeDetail()['status']
2287 if status in ('MERGED', 'ABANDONED'):
2288 DieWithError('Change %s has been %s, new uploads are not allowed' %
2289 (self.GetIssueURL(),
2290 'submitted' if status == 'MERGED' else 'abandoned'))
2291
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002292 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2293 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2294 # Apparently this check is not very important? Otherwise get_auth_email
2295 # could have been added to other implementations of Authenticator.
2296 cookies_auth = gerrit_util.Authenticator.get()
2297 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002298 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002299
2300 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002301 if self.GetIssueOwner() == cookies_user:
2302 return
2303 logging.debug('change %s owner is %s, cookies user is %s',
2304 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002305 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002306 # so ask what Gerrit thinks of this user.
2307 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2308 if details['email'] == self.GetIssueOwner():
2309 return
2310 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002311 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002312 'as %s.\n'
2313 'Uploading may fail due to lack of permissions.' %
2314 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2315 confirm_or_exit(action='upload')
2316
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002317 def _PostUnsetIssueProperties(self):
2318 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002319 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002320
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002321 def GetGerritObjForPresubmit(self):
2322 return presubmit_support.GerritAccessor(self._GetGerritHost())
2323
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002324 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002325 """Apply a rough heuristic to give a simple summary of an issue's review
2326 or CQ status, assuming adherence to a common workflow.
2327
2328 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002329 * 'error' - error from review tool (including deleted issues)
2330 * 'unsent' - no reviewers added
2331 * 'waiting' - waiting for review
2332 * 'reply' - waiting for uploader to reply to review
2333 * 'lgtm' - Code-Review label has been set
2334 * 'commit' - in the commit queue
2335 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002336 """
2337 if not self.GetIssue():
2338 return None
2339
2340 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002341 data = self._GetChangeDetail([
2342 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002343 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002344 return 'error'
2345
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002346 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002347 return 'closed'
2348
Aaron Gable9ab38c62017-04-06 14:36:33 -07002349 if data['labels'].get('Commit-Queue', {}).get('approved'):
2350 # The section will have an "approved" subsection if anyone has voted
2351 # the maximum value on the label.
2352 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002353
Aaron Gable9ab38c62017-04-06 14:36:33 -07002354 if data['labels'].get('Code-Review', {}).get('approved'):
2355 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002356
2357 if not data.get('reviewers', {}).get('REVIEWER', []):
2358 return 'unsent'
2359
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002360 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002361 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2362 last_message_author = messages.pop().get('author', {})
2363 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002364 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2365 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002366 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002367 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002368 if last_message_author.get('_account_id') == owner:
2369 # Most recent message was by owner.
2370 return 'waiting'
2371 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002372 # Some reply from non-owner.
2373 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002374
2375 # Somehow there are no messages even though there are reviewers.
2376 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002377
2378 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002379 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002380 patchset = data['revisions'][data['current_revision']]['_number']
2381 self.SetPatchset(patchset)
2382 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002383
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002384 def FetchDescription(self, force=False):
2385 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2386 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002387 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002388 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002389
dsansomee2d6fd92016-09-08 00:10:47 -07002390 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002391 if gerrit_util.HasPendingChangeEdit(
2392 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002393 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002394 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002395 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002396 'unpublished edit. Either publish the edit in the Gerrit web UI '
2397 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002398
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002399 gerrit_util.DeletePendingChangeEdit(
2400 self._GetGerritHost(), self._GerritChangeIdentifier())
2401 gerrit_util.SetCommitMessage(
2402 self._GetGerritHost(), self._GerritChangeIdentifier(),
2403 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002404
Aaron Gable636b13f2017-07-14 10:42:48 -07002405 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002406 gerrit_util.SetReview(
2407 self._GetGerritHost(), self._GerritChangeIdentifier(),
2408 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002409
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002410 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002411 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002412 messages = self._GetChangeDetail(
2413 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2414 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002415 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002416
2417 # Build dictionary of file comments for easy access and sorting later.
2418 # {author+date: {path: {patchset: {line: url+message}}}}
2419 comments = collections.defaultdict(
2420 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2421 for path, line_comments in file_comments.iteritems():
2422 for comment in line_comments:
2423 if comment.get('tag', '').startswith('autogenerated'):
2424 continue
2425 key = (comment['author']['email'], comment['updated'])
2426 if comment.get('side', 'REVISION') == 'PARENT':
2427 patchset = 'Base'
2428 else:
2429 patchset = 'PS%d' % comment['patch_set']
2430 line = comment.get('line', 0)
2431 url = ('https://%s/c/%s/%s/%s#%s%s' %
2432 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2433 'b' if comment.get('side') == 'PARENT' else '',
2434 str(line) if line else ''))
2435 comments[key][path][patchset][line] = (url, comment['message'])
2436
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002437 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002438 for msg in messages:
2439 # Don't bother showing autogenerated messages.
2440 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2441 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002442 # Gerrit spits out nanoseconds.
2443 assert len(msg['date'].split('.')[-1]) == 9
2444 date = datetime.datetime.strptime(msg['date'][:-3],
2445 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002446 message = msg['message']
2447 key = (msg['author']['email'], msg['date'])
2448 if key in comments:
2449 message += '\n'
2450 for path, patchsets in sorted(comments.get(key, {}).items()):
2451 if readable:
2452 message += '\n%s' % path
2453 for patchset, lines in sorted(patchsets.items()):
2454 for line, (url, content) in sorted(lines.items()):
2455 if line:
2456 line_str = 'Line %d' % line
2457 path_str = '%s:%d:' % (path, line)
2458 else:
2459 line_str = 'File comment'
2460 path_str = '%s:0:' % path
2461 if readable:
2462 message += '\n %s, %s: %s' % (patchset, line_str, url)
2463 message += '\n %s\n' % content
2464 else:
2465 message += '\n%s ' % path_str
2466 message += '\n%s\n' % content
2467
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002468 summary.append(_CommentSummary(
2469 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002470 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002471 sender=msg['author']['email'],
2472 # These could be inferred from the text messages and correlated with
2473 # Code-Review label maximum, however this is not reliable.
2474 # Leaving as is until the need arises.
2475 approval=False,
2476 disapproval=False,
2477 ))
2478 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002479
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002480 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002481 gerrit_util.AbandonChange(
2482 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002483
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002484 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002485 gerrit_util.SubmitChange(
2486 self._GetGerritHost(), self._GerritChangeIdentifier(),
2487 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002488
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002489 def _GetChangeDetail(self, options=None, no_cache=False):
2490 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002491
2492 If fresh data is needed, set no_cache=True which will clear cache and
2493 thus new data will be fetched from Gerrit.
2494 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002495 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002496 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002497
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002498 # Optimization to avoid multiple RPCs:
2499 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2500 'CURRENT_COMMIT' not in options):
2501 options.append('CURRENT_COMMIT')
2502
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002503 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002504 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002505 options = [o.upper() for o in options]
2506
2507 # Check in cache first unless no_cache is True.
2508 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002509 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002510 else:
2511 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002512 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002513 # Assumption: data fetched before with extra options is suitable
2514 # for return for a smaller set of options.
2515 # For example, if we cached data for
2516 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2517 # and request is for options=[CURRENT_REVISION],
2518 # THEN we can return prior cached data.
2519 if options_set.issubset(cached_options_set):
2520 return data
2521
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002522 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002523 data = gerrit_util.GetChangeDetail(
2524 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002525 except gerrit_util.GerritError as e:
2526 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002527 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002528 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002529
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002530 self._detail_cache.setdefault(cache_key, []).append(
2531 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002532 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002533
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002534 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002535 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002536 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002537 data = gerrit_util.GetChangeCommit(
2538 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002539 except gerrit_util.GerritError as e:
2540 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002541 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002542 raise
agable32978d92016-11-01 12:55:02 -07002543 return data
2544
Olivier Robin75ee7252018-04-13 10:02:56 +02002545 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002546 if git_common.is_dirty_git_tree('land'):
2547 return 1
tandriid60367b2016-06-22 05:25:12 -07002548 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2549 if u'Commit-Queue' in detail.get('labels', {}):
2550 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002551 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2552 'which can test and land changes for you. '
2553 'Are you sure you wish to bypass it?\n',
2554 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002555
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002556 differs = True
tandriic4344b52016-08-29 06:04:54 -07002557 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002558 # Note: git diff outputs nothing if there is no diff.
2559 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002560 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002561 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002562 if detail['current_revision'] == last_upload:
2563 differs = False
2564 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002565 print('WARNING: Local branch contents differ from latest uploaded '
2566 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002567 if differs:
2568 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002569 confirm_or_exit(
2570 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2571 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002572 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002573 elif not bypass_hooks:
2574 hook_results = self.RunHook(
2575 committing=True,
2576 may_prompt=not force,
2577 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002578 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2579 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002580 if not hook_results.should_continue():
2581 return 1
2582
2583 self.SubmitIssue(wait_for_merge=True)
2584 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002585 links = self._GetChangeCommit().get('web_links', [])
2586 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002587 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002588 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002589 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002590 return 0
2591
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002592 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002593 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002594 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002595 assert not directory
2596 assert parsed_issue_arg.valid
2597
2598 self._changelist.issue = parsed_issue_arg.issue
2599
2600 if parsed_issue_arg.hostname:
2601 self._gerrit_host = parsed_issue_arg.hostname
2602 self._gerrit_server = 'https://%s' % self._gerrit_host
2603
tandriic2405f52016-10-10 08:13:15 -07002604 try:
2605 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002606 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002607 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002608
2609 if not parsed_issue_arg.patchset:
2610 # Use current revision by default.
2611 revision_info = detail['revisions'][detail['current_revision']]
2612 patchset = int(revision_info['_number'])
2613 else:
2614 patchset = parsed_issue_arg.patchset
2615 for revision_info in detail['revisions'].itervalues():
2616 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2617 break
2618 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002619 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002620 (parsed_issue_arg.patchset, self.GetIssue()))
2621
Aaron Gable697a91b2018-01-19 15:20:15 -08002622 remote_url = self._changelist.GetRemoteUrl()
2623 if remote_url.endswith('.git'):
2624 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002625 remote_url = remote_url.rstrip('/')
2626
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002627 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002628 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002629
2630 if remote_url != fetch_info['url']:
2631 DieWithError('Trying to patch a change from %s but this repo appears '
2632 'to be %s.' % (fetch_info['url'], remote_url))
2633
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002634 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002635
Aaron Gable62619a32017-06-16 08:22:09 -07002636 if force:
2637 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2638 print('Checked out commit for change %i patchset %i locally' %
2639 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002640 elif nocommit:
2641 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2642 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002643 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002644 RunGit(['cherry-pick', 'FETCH_HEAD'])
2645 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002646 (parsed_issue_arg.issue, patchset))
2647 print('Note: this created a local commit which does not have '
2648 'the same hash as the one uploaded for review. This will make '
2649 'uploading changes based on top of this branch difficult.\n'
2650 'If you want to do that, use "git cl patch --force" instead.')
2651
Stefan Zagerd08043c2017-10-12 12:07:02 -07002652 if self.GetBranch():
2653 self.SetIssue(parsed_issue_arg.issue)
2654 self.SetPatchset(patchset)
2655 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2656 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2657 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2658 else:
2659 print('WARNING: You are in detached HEAD state.\n'
2660 'The patch has been applied to your checkout, but you will not be '
2661 'able to upload a new patch set to the gerrit issue.\n'
2662 'Try using the \'-b\' option if you would like to work on a '
2663 'branch and/or upload a new patch set.')
2664
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002665 return 0
2666
2667 @staticmethod
2668 def ParseIssueURL(parsed_url):
2669 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2670 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002671 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2672 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002673 # Short urls like https://domain/<issue_number> can be used, but don't allow
2674 # specifying the patchset (you'd 404), but we allow that here.
2675 if parsed_url.path == '/':
2676 part = parsed_url.fragment
2677 else:
2678 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002679 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002680 if match:
2681 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002682 issue=int(match.group(3)),
2683 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002684 hostname=parsed_url.netloc,
2685 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002686 return None
2687
tandrii16e0b4e2016-06-07 10:34:28 -07002688 def _GerritCommitMsgHookCheck(self, offer_removal):
2689 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2690 if not os.path.exists(hook):
2691 return
2692 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2693 # custom developer made one.
2694 data = gclient_utils.FileRead(hook)
2695 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2696 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002697 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002698 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002699 'and may interfere with it in subtle ways.\n'
2700 'We recommend you remove the commit-msg hook.')
2701 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002702 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002703 gclient_utils.rm_file_or_tree(hook)
2704 print('Gerrit commit-msg hook removed.')
2705 else:
2706 print('OK, will keep Gerrit commit-msg hook in place.')
2707
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002708 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002709 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002710 if options.squash and options.no_squash:
2711 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002712
2713 if not options.squash and not options.no_squash:
2714 # Load default for user, repo, squash=true, in this order.
2715 options.squash = settings.GetSquashGerritUploads()
2716 elif options.no_squash:
2717 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002718
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002719 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002720 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002721
Aaron Gableb56ad332017-01-06 15:24:31 -08002722 # This may be None; default fallback value is determined in logic below.
2723 title = options.title
2724
Dominic Battre7d1c4842017-10-27 09:17:28 +02002725 # Extract bug number from branch name.
2726 bug = options.bug
2727 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2728 if not bug and match:
2729 bug = match.group(1)
2730
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002731 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002732 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002733 if self.GetIssue():
2734 # Try to get the message from a previous upload.
2735 message = self.GetDescription()
2736 if not message:
2737 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002738 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002739 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002740 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002741 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002742 # When uploading a subsequent patchset, -m|--message is taken
2743 # as the patchset title if --title was not provided.
2744 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002745 else:
2746 default_title = RunGit(
2747 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002748 if options.force:
2749 title = default_title
2750 else:
2751 title = ask_for_data(
2752 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002753 change_id = self._GetChangeDetail()['change_id']
2754 while True:
2755 footer_change_ids = git_footers.get_footer_change_id(message)
2756 if footer_change_ids == [change_id]:
2757 break
2758 if not footer_change_ids:
2759 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002760 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002761 continue
2762 # There is already a valid footer but with different or several ids.
2763 # Doing this automatically is non-trivial as we don't want to lose
2764 # existing other footers, yet we want to append just 1 desired
2765 # Change-Id. Thus, just create a new footer, but let user verify the
2766 # new description.
2767 message = '%s\n\nChange-Id: %s' % (message, change_id)
2768 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002769 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002770 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002771 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002772 'Please, check the proposed correction to the description, '
2773 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2774 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2775 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002776 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002777 if not options.force:
2778 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002779 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002780 message = change_desc.description
2781 if not message:
2782 DieWithError("Description is empty. Aborting...")
2783 # Continue the while loop.
2784 # Sanity check of this code - we should end up with proper message
2785 # footer.
2786 assert [change_id] == git_footers.get_footer_change_id(message)
2787 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002788 else: # if not self.GetIssue()
2789 if options.message:
2790 message = options.message
2791 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002792 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002793 if options.title:
2794 message = options.title + '\n\n' + message
2795 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002796
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002797 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002798 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002799 # On first upload, patchset title is always this string, while
2800 # --title flag gets converted to first line of message.
2801 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002802 if not change_desc.description:
2803 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002804 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002805 if len(change_ids) > 1:
2806 DieWithError('too many Change-Id footers, at most 1 allowed.')
2807 if not change_ids:
2808 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002809 change_desc.set_description(git_footers.add_footer_change_id(
2810 change_desc.description,
2811 GenerateGerritChangeId(change_desc.description)))
2812 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002813 assert len(change_ids) == 1
2814 change_id = change_ids[0]
2815
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002816 if options.reviewers or options.tbrs or options.add_owners_to:
2817 change_desc.update_reviewers(options.reviewers, options.tbrs,
2818 options.add_owners_to, change)
2819
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002820 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002821 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2822 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002823 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002824 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2825 desc_tempfile.write(change_desc.description)
2826 desc_tempfile.close()
2827 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2828 '-F', desc_tempfile.name]).strip()
2829 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002830 else:
2831 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002832 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002833 if not change_desc.description:
2834 DieWithError("Description is empty. Aborting...")
2835
2836 if not git_footers.get_footer_change_id(change_desc.description):
2837 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002838 change_desc.set_description(
2839 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002840 if options.reviewers or options.tbrs or options.add_owners_to:
2841 change_desc.update_reviewers(options.reviewers, options.tbrs,
2842 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002843 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002844 # For no-squash mode, we assume the remote called "origin" is the one we
2845 # want. It is not worthwhile to support different workflows for
2846 # no-squash mode.
2847 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002848 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2849
2850 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002851 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002852 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2853 ref_to_push)]).splitlines()
2854 if len(commits) > 1:
2855 print('WARNING: This will upload %d commits. Run the following command '
2856 'to see which commits will be uploaded: ' % len(commits))
2857 print('git log %s..%s' % (parent, ref_to_push))
2858 print('You can also use `git squash-branch` to squash these into a '
2859 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002860 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002861
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002862 if options.reviewers or options.tbrs or options.add_owners_to:
2863 change_desc.update_reviewers(options.reviewers, options.tbrs,
2864 options.add_owners_to, change)
2865
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002866 reviewers = sorted(change_desc.get_reviewers())
2867 # Add cc's from the CC_LIST and --cc flag (if any).
2868 if not options.private and not options.no_autocc:
2869 cc = self.GetCCList().split(',')
2870 else:
2871 cc = []
2872 if options.cc:
2873 cc.extend(options.cc)
2874 cc = filter(None, [email.strip() for email in cc])
2875 if change_desc.get_cced():
2876 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002877 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2878 valid_accounts = set(reviewers + cc)
2879 # TODO(crbug/877717): relax this for all hosts.
2880 else:
2881 valid_accounts = gerrit_util.ValidAccounts(
2882 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002883 logging.info('accounts %s are recognized, %s invalid',
2884 sorted(valid_accounts),
2885 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002886
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002887 # Extra options that can be specified at push time. Doc:
2888 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002889 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002890
Aaron Gable844cf292017-06-28 11:32:59 -07002891 # By default, new changes are started in WIP mode, and subsequent patchsets
2892 # don't send email. At any time, passing --send-mail will mark the change
2893 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002894 if options.send_mail:
2895 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002896 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002897 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002898 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002899 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002900 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002901
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002902 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002903 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002904
Aaron Gable9b713dd2016-12-14 16:04:21 -08002905 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002906 # Punctuation and whitespace in |title| must be percent-encoded.
2907 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002908
agablec6787972016-09-09 16:13:34 -07002909 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002910 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002911
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002912 for r in sorted(reviewers):
2913 if r in valid_accounts:
2914 refspec_opts.append('r=%s' % r)
2915 reviewers.remove(r)
2916 else:
2917 # TODO(tandrii): this should probably be a hard failure.
2918 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2919 % r)
2920 for c in sorted(cc):
2921 # refspec option will be rejected if cc doesn't correspond to an
2922 # account, even though REST call to add such arbitrary cc may succeed.
2923 if c in valid_accounts:
2924 refspec_opts.append('cc=%s' % c)
2925 cc.remove(c)
2926
rmistry9eadede2016-09-19 11:22:43 -07002927 if options.topic:
2928 # Documentation on Gerrit topics is here:
2929 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002930 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002931
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002932 if not change_desc.get_reviewers(tbr_only=True):
2933 # Change is not TBR, so we can inline setting other labels, too.
2934 # TODO(crbug.com/877717): make this working for TBR, too, by figuring out
2935 # max score for CR label somehow.
2936 if options.enable_auto_submit:
2937 refspec_opts.append('l=Auto-Submit+1')
2938 if options.use_commit_queue:
2939 refspec_opts.append('l=Commit-Queue+2')
2940 elif options.cq_dry_run:
2941 refspec_opts.append('l=Commit-Queue+1')
2942
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002943 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002944 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002945 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002946 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002947 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2948
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002949 refspec_suffix = ''
2950 if refspec_opts:
2951 refspec_suffix = '%' + ','.join(refspec_opts)
2952 assert ' ' not in refspec_suffix, (
2953 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2954 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2955
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002956 try:
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002957 before_push = time_time()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002958 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00002959 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemur83bd7f42018-10-10 00:14:21 +00002960 print_stdout=False,
Edward Lemur49c8eaf2018-11-07 22:13:12 +00002961 # Flush after every line: useful for seeing progress when running as
2962 # recipe.
2963 filter_fn=lambda _: sys.stdout.flush())
2964 push_returncode = 0
Edward Lemurfec80c42018-11-01 23:14:14 +00002965 except subprocess2.CalledProcessError as e:
2966 push_returncode = e.returncode
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002967 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002968 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002969 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002970 'credential problems:\n'
2971 ' git cl creds-check\n',
2972 change_desc)
Edward Lemurfec80c42018-11-01 23:14:14 +00002973 finally:
2974 metrics.collector.add_repeated('sub_commands', {
2975 'command': 'git push',
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002976 'execution_time': time_time() - before_push,
Edward Lemurfec80c42018-11-01 23:14:14 +00002977 'exit_code': push_returncode,
2978 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2979 })
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002980
2981 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002982 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002983 change_numbers = [m.group(1)
2984 for m in map(regex.match, push_stdout.splitlines())
2985 if m]
2986 if len(change_numbers) != 1:
2987 DieWithError(
2988 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002989 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002990 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002991 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002992
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002993 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002994 # GetIssue() is not set in case of non-squash uploads according to tests.
2995 # TODO(agable): non-squash uploads in git cl should be removed.
2996 gerrit_util.AddReviewers(
2997 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002998 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002999 reviewers, cc,
3000 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003001
Aaron Gablefd238082017-06-07 13:42:34 -07003002 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003003 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3004 score = 1
3005 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3006 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3007 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003008 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003009 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003010 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003011 msg='Self-approving for TBR',
3012 labels={'Code-Review': score})
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00003013 # Labels aren't set through refspec only if tbr is set (see check above).
3014 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
3015 options.cq_dry_run)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003016 return 0
3017
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003018 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3019 change_desc):
3020 """Computes parent of the generated commit to be uploaded to Gerrit.
3021
3022 Returns revision or a ref name.
3023 """
3024 if custom_cl_base:
3025 # Try to avoid creating additional unintended CLs when uploading, unless
3026 # user wants to take this risk.
3027 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3028 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3029 local_ref_of_target_remote])
3030 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003031 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003032 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3033 'If you proceed with upload, more than 1 CL may be created by '
3034 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3035 'If you are certain that specified base `%s` has already been '
3036 'uploaded to Gerrit as another CL, you may proceed.\n' %
3037 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3038 if not force:
3039 confirm_or_exit(
3040 'Do you take responsibility for cleaning up potential mess '
3041 'resulting from proceeding with upload?',
3042 action='upload')
3043 return custom_cl_base
3044
Aaron Gablef97e33d2017-03-30 15:44:27 -07003045 if remote != '.':
3046 return self.GetCommonAncestorWithUpstream()
3047
3048 # If our upstream branch is local, we base our squashed commit on its
3049 # squashed version.
3050 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3051
Aaron Gablef97e33d2017-03-30 15:44:27 -07003052 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003053 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003054
3055 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003056 # TODO(tandrii): consider checking parent change in Gerrit and using its
3057 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3058 # the tree hash of the parent branch. The upside is less likely bogus
3059 # requests to reupload parent change just because it's uploadhash is
3060 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003061 parent = RunGit(['config',
3062 'branch.%s.gerritsquashhash' % upstream_branch_name],
3063 error_ok=True).strip()
3064 # Verify that the upstream branch has been uploaded too, otherwise
3065 # Gerrit will create additional CLs when uploading.
3066 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3067 RunGitSilent(['rev-parse', parent + ':'])):
3068 DieWithError(
3069 '\nUpload upstream branch %s first.\n'
3070 'It is likely that this branch has been rebased since its last '
3071 'upload, so you just need to upload it again.\n'
3072 '(If you uploaded it with --no-squash, then branch dependencies '
3073 'are not supported, and you should reupload with --squash.)'
3074 % upstream_branch_name,
3075 change_desc)
3076 return parent
3077
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003078 def _AddChangeIdToCommitMessage(self, options, args):
3079 """Re-commits using the current message, assumes the commit hook is in
3080 place.
3081 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003082 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003083 git_command = ['commit', '--amend', '-m', log_desc]
3084 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003085 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003086 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003087 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003088 return new_log_desc
3089 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003090 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003091
Ravi Mistry31e7d562018-04-02 12:53:57 -04003092 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3093 """Sets labels on the change based on the provided flags."""
3094 labels = {}
3095 notify = None;
3096 if enable_auto_submit:
3097 labels['Auto-Submit'] = 1
3098 if use_commit_queue:
3099 labels['Commit-Queue'] = 2
3100 elif cq_dry_run:
3101 labels['Commit-Queue'] = 1
3102 notify = False
3103 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003104 gerrit_util.SetReview(
3105 self._GetGerritHost(),
3106 self._GerritChangeIdentifier(),
3107 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003108
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003109 def SetCQState(self, new_state):
3110 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003111 vote_map = {
3112 _CQState.NONE: 0,
3113 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003114 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003115 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003116 labels = {'Commit-Queue': vote_map[new_state]}
3117 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003118 gerrit_util.SetReview(
3119 self._GetGerritHost(), self._GerritChangeIdentifier(),
3120 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003121
tandriie113dfd2016-10-11 10:20:12 -07003122 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003123 try:
3124 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003125 except GerritChangeNotExists:
3126 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003127
3128 if data['status'] in ('ABANDONED', 'MERGED'):
3129 return 'CL %s is closed' % self.GetIssue()
3130
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003131 def GetTryJobProperties(self, patchset=None):
3132 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003133 data = self._GetChangeDetail(['ALL_REVISIONS'])
3134 patchset = int(patchset or self.GetPatchset())
3135 assert patchset
3136 revision_data = None # Pylint wants it to be defined.
3137 for revision_data in data['revisions'].itervalues():
3138 if int(revision_data['_number']) == patchset:
3139 break
3140 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003141 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003142 (patchset, self.GetIssue()))
3143 return {
3144 'patch_issue': self.GetIssue(),
3145 'patch_set': patchset or self.GetPatchset(),
3146 'patch_project': data['project'],
3147 'patch_storage': 'gerrit',
3148 'patch_ref': revision_data['fetch']['http']['ref'],
3149 'patch_repository_url': revision_data['fetch']['http']['url'],
3150 'patch_gerrit_url': self.GetCodereviewServer(),
3151 }
tandriie113dfd2016-10-11 10:20:12 -07003152
tandriide281ae2016-10-12 06:02:30 -07003153 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003154 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003155
Edward Lemur707d70b2018-02-07 00:50:14 +01003156 def GetReviewers(self):
3157 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3158 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3159
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003160
3161_CODEREVIEW_IMPLEMENTATIONS = {
3162 'rietveld': _RietveldChangelistImpl,
3163 'gerrit': _GerritChangelistImpl,
3164}
3165
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003166
iannuccie53c9352016-08-17 14:40:40 -07003167def _add_codereview_issue_select_options(parser, extra=""):
3168 _add_codereview_select_options(parser)
3169
3170 text = ('Operate on this issue number instead of the current branch\'s '
3171 'implicit issue.')
3172 if extra:
3173 text += ' '+extra
3174 parser.add_option('-i', '--issue', type=int, help=text)
3175
3176
3177def _process_codereview_issue_select_options(parser, options):
3178 _process_codereview_select_options(parser, options)
3179 if options.issue is not None and not options.forced_codereview:
3180 parser.error('--issue must be specified with either --rietveld or --gerrit')
3181
3182
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003183def _add_codereview_select_options(parser):
3184 """Appends --gerrit and --rietveld options to force specific codereview."""
3185 parser.codereview_group = optparse.OptionGroup(
3186 parser, 'EXPERIMENTAL! Codereview override options')
3187 parser.add_option_group(parser.codereview_group)
3188 parser.codereview_group.add_option(
3189 '--gerrit', action='store_true',
3190 help='Force the use of Gerrit for codereview')
3191 parser.codereview_group.add_option(
3192 '--rietveld', action='store_true',
3193 help='Force the use of Rietveld for codereview')
3194
3195
3196def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003197 if options.rietveld:
3198 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003199 options.forced_codereview = None
3200 if options.gerrit:
3201 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003202
3203
tandriif9aefb72016-07-01 09:06:51 -07003204def _get_bug_line_values(default_project, bugs):
3205 """Given default_project and comma separated list of bugs, yields bug line
3206 values.
3207
3208 Each bug can be either:
3209 * a number, which is combined with default_project
3210 * string, which is left as is.
3211
3212 This function may produce more than one line, because bugdroid expects one
3213 project per line.
3214
3215 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3216 ['v8:123', 'chromium:789']
3217 """
3218 default_bugs = []
3219 others = []
3220 for bug in bugs.split(','):
3221 bug = bug.strip()
3222 if bug:
3223 try:
3224 default_bugs.append(int(bug))
3225 except ValueError:
3226 others.append(bug)
3227
3228 if default_bugs:
3229 default_bugs = ','.join(map(str, default_bugs))
3230 if default_project:
3231 yield '%s:%s' % (default_project, default_bugs)
3232 else:
3233 yield default_bugs
3234 for other in sorted(others):
3235 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3236 yield other
3237
3238
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003239class ChangeDescription(object):
3240 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003241 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003242 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003243 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003244 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003245 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3246 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3247 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3248 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003249
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003250 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003251 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003252
agable@chromium.org42c20792013-09-12 17:34:49 +00003253 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003254 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003255 return '\n'.join(self._description_lines)
3256
3257 def set_description(self, desc):
3258 if isinstance(desc, basestring):
3259 lines = desc.splitlines()
3260 else:
3261 lines = [line.rstrip() for line in desc]
3262 while lines and not lines[0]:
3263 lines.pop(0)
3264 while lines and not lines[-1]:
3265 lines.pop(-1)
3266 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003267
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003268 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3269 """Rewrites the R=/TBR= line(s) as a single line each.
3270
3271 Args:
3272 reviewers (list(str)) - list of additional emails to use for reviewers.
3273 tbrs (list(str)) - list of additional emails to use for TBRs.
3274 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3275 the change that are missing OWNER coverage. If this is not None, you
3276 must also pass a value for `change`.
3277 change (Change) - The Change that should be used for OWNERS lookups.
3278 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003279 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003280 assert isinstance(tbrs, list), tbrs
3281
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003282 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003283 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003284
3285 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003286 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003287
3288 reviewers = set(reviewers)
3289 tbrs = set(tbrs)
3290 LOOKUP = {
3291 'TBR': tbrs,
3292 'R': reviewers,
3293 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003294
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003295 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003296 regexp = re.compile(self.R_LINE)
3297 matches = [regexp.match(line) for line in self._description_lines]
3298 new_desc = [l for i, l in enumerate(self._description_lines)
3299 if not matches[i]]
3300 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003301
agable@chromium.org42c20792013-09-12 17:34:49 +00003302 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003303
3304 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003305 for match in matches:
3306 if not match:
3307 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003308 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3309
3310 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003311 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003312 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003313 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003314 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003315 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003316 LOOKUP[add_owners_to].update(
3317 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003318
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003319 # If any folks ended up in both groups, remove them from tbrs.
3320 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003321
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003322 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3323 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003324
3325 # Put the new lines in the description where the old first R= line was.
3326 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3327 if 0 <= line_loc < len(self._description_lines):
3328 if new_tbr_line:
3329 self._description_lines.insert(line_loc, new_tbr_line)
3330 if new_r_line:
3331 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003332 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003333 if new_r_line:
3334 self.append_footer(new_r_line)
3335 if new_tbr_line:
3336 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003337
Aaron Gable3a16ed12017-03-23 10:51:55 -07003338 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003339 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003340 self.set_description([
3341 '# Enter a description of the change.',
3342 '# This will be displayed on the codereview site.',
3343 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003344 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003345 '--------------------',
3346 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003347
agable@chromium.org42c20792013-09-12 17:34:49 +00003348 regexp = re.compile(self.BUG_LINE)
3349 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003350 prefix = settings.GetBugPrefix()
3351 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003352 if git_footer:
3353 self.append_footer('Bug: %s' % ', '.join(values))
3354 else:
3355 for value in values:
3356 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003357
agable@chromium.org42c20792013-09-12 17:34:49 +00003358 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003359 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003360 if not content:
3361 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003362 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003363
Bruce Dawson2377b012018-01-11 16:46:49 -08003364 # Strip off comments and default inserted "Bug:" line.
3365 clean_lines = [line.rstrip() for line in lines if not
3366 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003367 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003368 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003369 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003370
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003371 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003372 """Adds a footer line to the description.
3373
3374 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3375 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3376 that Gerrit footers are always at the end.
3377 """
3378 parsed_footer_line = git_footers.parse_footer(line)
3379 if parsed_footer_line:
3380 # Line is a gerrit footer in the form: Footer-Key: any value.
3381 # Thus, must be appended observing Gerrit footer rules.
3382 self.set_description(
3383 git_footers.add_footer(self.description,
3384 key=parsed_footer_line[0],
3385 value=parsed_footer_line[1]))
3386 return
3387
3388 if not self._description_lines:
3389 self._description_lines.append(line)
3390 return
3391
3392 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3393 if gerrit_footers:
3394 # git_footers.split_footers ensures that there is an empty line before
3395 # actual (gerrit) footers, if any. We have to keep it that way.
3396 assert top_lines and top_lines[-1] == ''
3397 top_lines, separator = top_lines[:-1], top_lines[-1:]
3398 else:
3399 separator = [] # No need for separator if there are no gerrit_footers.
3400
3401 prev_line = top_lines[-1] if top_lines else ''
3402 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3403 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3404 top_lines.append('')
3405 top_lines.append(line)
3406 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003407
tandrii99a72f22016-08-17 14:33:24 -07003408 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003409 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003410 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003411 reviewers = [match.group(2).strip()
3412 for match in matches
3413 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003414 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003415
bradnelsond975b302016-10-23 12:20:23 -07003416 def get_cced(self):
3417 """Retrieves the list of reviewers."""
3418 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3419 cced = [match.group(2).strip() for match in matches if match]
3420 return cleanup_list(cced)
3421
Nodir Turakulov23b82142017-11-16 11:04:25 -08003422 def get_hash_tags(self):
3423 """Extracts and sanitizes a list of Gerrit hashtags."""
3424 subject = (self._description_lines or ('',))[0]
3425 subject = re.sub(
3426 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3427
3428 tags = []
3429 start = 0
3430 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3431 while True:
3432 m = bracket_exp.match(subject, start)
3433 if not m:
3434 break
3435 tags.append(self.sanitize_hash_tag(m.group(1)))
3436 start = m.end()
3437
3438 if not tags:
3439 # Try "Tag: " prefix.
3440 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3441 if m:
3442 tags.append(self.sanitize_hash_tag(m.group(1)))
3443 return tags
3444
3445 @classmethod
3446 def sanitize_hash_tag(cls, tag):
3447 """Returns a sanitized Gerrit hash tag.
3448
3449 A sanitized hashtag can be used as a git push refspec parameter value.
3450 """
3451 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3452
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003453 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3454 """Updates this commit description given the parent.
3455
3456 This is essentially what Gnumbd used to do.
3457 Consult https://goo.gl/WMmpDe for more details.
3458 """
3459 assert parent_msg # No, orphan branch creation isn't supported.
3460 assert parent_hash
3461 assert dest_ref
3462 parent_footer_map = git_footers.parse_footers(parent_msg)
3463 # This will also happily parse svn-position, which GnumbD is no longer
3464 # supporting. While we'd generate correct footers, the verifier plugin
3465 # installed in Gerrit will block such commit (ie git push below will fail).
3466 parent_position = git_footers.get_position(parent_footer_map)
3467
3468 # Cherry-picks may have last line obscuring their prior footers,
3469 # from git_footers perspective. This is also what Gnumbd did.
3470 cp_line = None
3471 if (self._description_lines and
3472 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3473 cp_line = self._description_lines.pop()
3474
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003475 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003476
3477 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3478 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003479 for i, line in enumerate(footer_lines):
3480 k, v = git_footers.parse_footer(line) or (None, None)
3481 if k and k.startswith('Cr-'):
3482 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003483
3484 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003485 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003486 if parent_position[0] == dest_ref:
3487 # Same branch as parent.
3488 number = int(parent_position[1]) + 1
3489 else:
3490 number = 1 # New branch, and extra lineage.
3491 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3492 int(parent_position[1])))
3493
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003494 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3495 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003496
3497 self._description_lines = top_lines
3498 if cp_line:
3499 self._description_lines.append(cp_line)
3500 if self._description_lines[-1] != '':
3501 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003502 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003503
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003504
Aaron Gablea1bab272017-04-11 16:38:18 -07003505def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003506 """Retrieves the reviewers that approved a CL from the issue properties with
3507 messages.
3508
3509 Note that the list may contain reviewers that are not committer, thus are not
3510 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003511
3512 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003513 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003514 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003515 return sorted(
3516 set(
3517 message['sender']
3518 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003519 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003520 )
3521 )
3522
3523
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003524def FindCodereviewSettingsFile(filename='codereview.settings'):
3525 """Finds the given file starting in the cwd and going up.
3526
3527 Only looks up to the top of the repository unless an
3528 'inherit-review-settings-ok' file exists in the root of the repository.
3529 """
3530 inherit_ok_file = 'inherit-review-settings-ok'
3531 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003532 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003533 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3534 root = '/'
3535 while True:
3536 if filename in os.listdir(cwd):
3537 if os.path.isfile(os.path.join(cwd, filename)):
3538 return open(os.path.join(cwd, filename))
3539 if cwd == root:
3540 break
3541 cwd = os.path.dirname(cwd)
3542
3543
3544def LoadCodereviewSettingsFromFile(fileobj):
3545 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003546 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003547
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003548 def SetProperty(name, setting, unset_error_ok=False):
3549 fullname = 'rietveld.' + name
3550 if setting in keyvals:
3551 RunGit(['config', fullname, keyvals[setting]])
3552 else:
3553 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3554
tandrii48df5812016-10-17 03:55:37 -07003555 if not keyvals.get('GERRIT_HOST', False):
3556 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003557 # Only server setting is required. Other settings can be absent.
3558 # In that case, we ignore errors raised during option deletion attempt.
3559 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003560 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003561 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3562 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003563 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003564 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3565 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003566 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003567 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3568 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003569
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003570 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003571 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003572
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003573 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003574 RunGit(['config', 'gerrit.squash-uploads',
3575 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003576
tandrii@chromium.org28253532016-04-14 13:46:56 +00003577 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003578 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003579 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3580
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003581 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003582 # should be of the form
3583 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3584 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003585 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3586 keyvals['ORIGIN_URL_CONFIG']])
3587
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003588
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003589def urlretrieve(source, destination):
3590 """urllib is broken for SSL connections via a proxy therefore we
3591 can't use urllib.urlretrieve()."""
3592 with open(destination, 'w') as f:
3593 f.write(urllib2.urlopen(source).read())
3594
3595
ukai@chromium.org712d6102013-11-27 00:52:58 +00003596def hasSheBang(fname):
3597 """Checks fname is a #! script."""
3598 with open(fname) as f:
3599 return f.read(2).startswith('#!')
3600
3601
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003602# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3603def DownloadHooks(*args, **kwargs):
3604 pass
3605
3606
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003607def DownloadGerritHook(force):
3608 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003609
3610 Args:
3611 force: True to update hooks. False to install hooks if not present.
3612 """
3613 if not settings.GetIsGerrit():
3614 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003615 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003616 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3617 if not os.access(dst, os.X_OK):
3618 if os.path.exists(dst):
3619 if not force:
3620 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003621 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003622 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003623 if not hasSheBang(dst):
3624 DieWithError('Not a script: %s\n'
3625 'You need to download from\n%s\n'
3626 'into .git/hooks/commit-msg and '
3627 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003628 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3629 except Exception:
3630 if os.path.exists(dst):
3631 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003632 DieWithError('\nFailed to download hooks.\n'
3633 'You need to download from\n%s\n'
3634 'into .git/hooks/commit-msg and '
3635 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003636
3637
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003638def GetRietveldCodereviewSettingsInteractively():
3639 """Prompt the user for settings."""
3640 server = settings.GetDefaultServerUrl(error_ok=True)
3641 prompt = 'Rietveld server (host[:port])'
3642 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3643 newserver = ask_for_data(prompt + ':')
3644 if not server and not newserver:
3645 newserver = DEFAULT_SERVER
3646 if newserver:
3647 newserver = gclient_utils.UpgradeToHttps(newserver)
3648 if newserver != server:
3649 RunGit(['config', 'rietveld.server', newserver])
3650
3651 def SetProperty(initial, caption, name, is_url):
3652 prompt = caption
3653 if initial:
3654 prompt += ' ("x" to clear) [%s]' % initial
3655 new_val = ask_for_data(prompt + ':')
3656 if new_val == 'x':
3657 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3658 elif new_val:
3659 if is_url:
3660 new_val = gclient_utils.UpgradeToHttps(new_val)
3661 if new_val != initial:
3662 RunGit(['config', 'rietveld.' + name, new_val])
3663
3664 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3665 SetProperty(settings.GetDefaultPrivateFlag(),
3666 'Private flag (rietveld only)', 'private', False)
3667 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3668 'tree-status-url', False)
3669 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3670 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3671 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3672 'run-post-upload-hook', False)
3673
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003674
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003675class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003676 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003677
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003678 _GOOGLESOURCE = 'googlesource.com'
3679
3680 def __init__(self):
3681 # Cached list of [host, identity, source], where source is either
3682 # .gitcookies or .netrc.
3683 self._all_hosts = None
3684
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003685 def ensure_configured_gitcookies(self):
3686 """Runs checks and suggests fixes to make git use .gitcookies from default
3687 path."""
3688 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3689 configured_path = RunGitSilent(
3690 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003691 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003692 if configured_path:
3693 self._ensure_default_gitcookies_path(configured_path, default)
3694 else:
3695 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003696
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003697 @staticmethod
3698 def _ensure_default_gitcookies_path(configured_path, default_path):
3699 assert configured_path
3700 if configured_path == default_path:
3701 print('git is already configured to use your .gitcookies from %s' %
3702 configured_path)
3703 return
3704
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003705 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003706 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3707 (configured_path, default_path))
3708
3709 if not os.path.exists(configured_path):
3710 print('However, your configured .gitcookies file is missing.')
3711 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3712 action='reconfigure')
3713 RunGit(['config', '--global', 'http.cookiefile', default_path])
3714 return
3715
3716 if os.path.exists(default_path):
3717 print('WARNING: default .gitcookies file already exists %s' %
3718 default_path)
3719 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3720 default_path)
3721
3722 confirm_or_exit('Move existing .gitcookies to default location?',
3723 action='move')
3724 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003725 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003726 print('Moved and reconfigured git to use .gitcookies from %s' %
3727 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003728
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003729 @staticmethod
3730 def _configure_gitcookies_path(default_path):
3731 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3732 if os.path.exists(netrc_path):
3733 print('You seem to be using outdated .netrc for git credentials: %s' %
3734 netrc_path)
3735 print('This tool will guide you through setting up recommended '
3736 '.gitcookies store for git credentials.\n'
3737 '\n'
3738 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3739 ' git config --global --unset http.cookiefile\n'
3740 ' mv %s %s.backup\n\n' % (default_path, default_path))
3741 confirm_or_exit(action='setup .gitcookies')
3742 RunGit(['config', '--global', 'http.cookiefile', default_path])
3743 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003744
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003745 def get_hosts_with_creds(self, include_netrc=False):
3746 if self._all_hosts is None:
3747 a = gerrit_util.CookiesAuthenticator()
3748 self._all_hosts = [
3749 (h, u, s)
3750 for h, u, s in itertools.chain(
3751 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3752 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3753 )
3754 if h.endswith(self._GOOGLESOURCE)
3755 ]
3756
3757 if include_netrc:
3758 return self._all_hosts
3759 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3760
3761 def print_current_creds(self, include_netrc=False):
3762 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3763 if not hosts:
3764 print('No Git/Gerrit credentials found')
3765 return
3766 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3767 header = [('Host', 'User', 'Which file'),
3768 ['=' * l for l in lengths]]
3769 for row in (header + hosts):
3770 print('\t'.join((('%%+%ds' % l) % s)
3771 for l, s in zip(lengths, row)))
3772
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003773 @staticmethod
3774 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003775 """Parses identity "git-<username>.domain" into <username> and domain."""
3776 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003777 # distinguishable from sub-domains. But we do know typical domains:
3778 if identity.endswith('.chromium.org'):
3779 domain = 'chromium.org'
3780 username = identity[:-len('.chromium.org')]
3781 else:
3782 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003783 if username.startswith('git-'):
3784 username = username[len('git-'):]
3785 return username, domain
3786
3787 def _get_usernames_of_domain(self, domain):
3788 """Returns list of usernames referenced by .gitcookies in a given domain."""
3789 identities_by_domain = {}
3790 for _, identity, _ in self.get_hosts_with_creds():
3791 username, domain = self._parse_identity(identity)
3792 identities_by_domain.setdefault(domain, []).append(username)
3793 return identities_by_domain.get(domain)
3794
3795 def _canonical_git_googlesource_host(self, host):
3796 """Normalizes Gerrit hosts (with '-review') to Git host."""
3797 assert host.endswith(self._GOOGLESOURCE)
3798 # Prefix doesn't include '.' at the end.
3799 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3800 if prefix.endswith('-review'):
3801 prefix = prefix[:-len('-review')]
3802 return prefix + '.' + self._GOOGLESOURCE
3803
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003804 def _canonical_gerrit_googlesource_host(self, host):
3805 git_host = self._canonical_git_googlesource_host(host)
3806 prefix = git_host.split('.', 1)[0]
3807 return prefix + '-review.' + self._GOOGLESOURCE
3808
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003809 def _get_counterpart_host(self, host):
3810 assert host.endswith(self._GOOGLESOURCE)
3811 git = self._canonical_git_googlesource_host(host)
3812 gerrit = self._canonical_gerrit_googlesource_host(git)
3813 return git if gerrit == host else gerrit
3814
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003815 def has_generic_host(self):
3816 """Returns whether generic .googlesource.com has been configured.
3817
3818 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3819 """
3820 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3821 if host == '.' + self._GOOGLESOURCE:
3822 return True
3823 return False
3824
3825 def _get_git_gerrit_identity_pairs(self):
3826 """Returns map from canonic host to pair of identities (Git, Gerrit).
3827
3828 One of identities might be None, meaning not configured.
3829 """
3830 host_to_identity_pairs = {}
3831 for host, identity, _ in self.get_hosts_with_creds():
3832 canonical = self._canonical_git_googlesource_host(host)
3833 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3834 idx = 0 if canonical == host else 1
3835 pair[idx] = identity
3836 return host_to_identity_pairs
3837
3838 def get_partially_configured_hosts(self):
3839 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003840 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3841 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3842 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003843
3844 def get_conflicting_hosts(self):
3845 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003846 host
3847 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003848 if None not in (i1, i2) and i1 != i2)
3849
3850 def get_duplicated_hosts(self):
3851 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3852 return set(host for host, count in counters.iteritems() if count > 1)
3853
3854 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3855 'chromium.googlesource.com': 'chromium.org',
3856 'chrome-internal.googlesource.com': 'google.com',
3857 }
3858
3859 def get_hosts_with_wrong_identities(self):
3860 """Finds hosts which **likely** reference wrong identities.
3861
3862 Note: skips hosts which have conflicting identities for Git and Gerrit.
3863 """
3864 hosts = set()
3865 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3866 pair = self._get_git_gerrit_identity_pairs().get(host)
3867 if pair and pair[0] == pair[1]:
3868 _, domain = self._parse_identity(pair[0])
3869 if domain != expected:
3870 hosts.add(host)
3871 return hosts
3872
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003873 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003874 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003875 hosts = sorted(hosts)
3876 assert hosts
3877 if extra_column_func is None:
3878 extras = [''] * len(hosts)
3879 else:
3880 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003881 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3882 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003883 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003884 lines.append(tmpl % he)
3885 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003886
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003887 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003888 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003889 yield ('.googlesource.com wildcard record detected',
3890 ['Chrome Infrastructure team recommends to list full host names '
3891 'explicitly.'],
3892 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003893
3894 dups = self.get_duplicated_hosts()
3895 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003896 yield ('The following hosts were defined twice',
3897 self._format_hosts(dups),
3898 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003899
3900 partial = self.get_partially_configured_hosts()
3901 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003902 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3903 'These hosts are missing',
3904 self._format_hosts(partial, lambda host: 'but %s defined' %
3905 self._get_counterpart_host(host)),
3906 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003907
3908 conflicting = self.get_conflicting_hosts()
3909 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003910 yield ('The following Git hosts have differing credentials from their '
3911 'Gerrit counterparts',
3912 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3913 tuple(self._get_git_gerrit_identity_pairs()[host])),
3914 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003915
3916 wrong = self.get_hosts_with_wrong_identities()
3917 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003918 yield ('These hosts likely use wrong identity',
3919 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3920 (self._get_git_gerrit_identity_pairs()[host][0],
3921 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3922 wrong)
3923
3924 def find_and_report_problems(self):
3925 """Returns True if there was at least one problem, else False."""
3926 found = False
3927 bad_hosts = set()
3928 for title, sublines, hosts in self._find_problems():
3929 if not found:
3930 found = True
3931 print('\n\n.gitcookies problem report:\n')
3932 bad_hosts.update(hosts or [])
3933 print(' %s%s' % (title , (':' if sublines else '')))
3934 if sublines:
3935 print()
3936 print(' %s' % '\n '.join(sublines))
3937 print()
3938
3939 if bad_hosts:
3940 assert found
3941 print(' You can manually remove corresponding lines in your %s file and '
3942 'visit the following URLs with correct account to generate '
3943 'correct credential lines:\n' %
3944 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3945 print(' %s' % '\n '.join(sorted(set(
3946 gerrit_util.CookiesAuthenticator().get_new_password_url(
3947 self._canonical_git_googlesource_host(host))
3948 for host in bad_hosts
3949 ))))
3950 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003951
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003952
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003953@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003954def CMDcreds_check(parser, args):
3955 """Checks credentials and suggests changes."""
3956 _, _ = parser.parse_args(args)
3957
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003958 # Code below checks .gitcookies. Abort if using something else.
3959 authn = gerrit_util.Authenticator.get()
3960 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3961 if isinstance(authn, gerrit_util.GceAuthenticator):
3962 DieWithError(
3963 'This command is not designed for GCE, are you on a bot?\n'
3964 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3965 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003966 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003967 'This command is not designed for bot environment. It checks '
3968 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003969
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003970 checker = _GitCookiesChecker()
3971 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003972
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003973 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003974 checker.print_current_creds(include_netrc=True)
3975
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003976 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003977 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003978 return 0
3979 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003980
3981
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003982@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003983@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003985 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003986
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003987 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07003988 # TODO(tandrii): remove this once we switch to Gerrit.
3989 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003990 parser.add_option('--activate-update', action='store_true',
3991 help='activate auto-updating [rietveld] section in '
3992 '.git/config')
3993 parser.add_option('--deactivate-update', action='store_true',
3994 help='deactivate auto-updating [rietveld] section in '
3995 '.git/config')
3996 options, args = parser.parse_args(args)
3997
3998 if options.deactivate_update:
3999 RunGit(['config', 'rietveld.autoupdate', 'false'])
4000 return
4001
4002 if options.activate_update:
4003 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4004 return
4005
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004006 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004007 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004008 return 0
4009
4010 url = args[0]
4011 if not url.endswith('codereview.settings'):
4012 url = os.path.join(url, 'codereview.settings')
4013
4014 # Load code review settings and download hooks (if available).
4015 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4016 return 0
4017
4018
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004019@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004020def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004021 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004022 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4023 branch = ShortBranchName(branchref)
4024 _, args = parser.parse_args(args)
4025 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004026 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004027 return RunGit(['config', 'branch.%s.base-url' % branch],
4028 error_ok=False).strip()
4029 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004030 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004031 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4032 error_ok=False).strip()
4033
4034
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004035def color_for_status(status):
4036 """Maps a Changelist status to color, for CMDstatus and other tools."""
4037 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004038 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004039 'waiting': Fore.BLUE,
4040 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004041 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004042 'lgtm': Fore.GREEN,
4043 'commit': Fore.MAGENTA,
4044 'closed': Fore.CYAN,
4045 'error': Fore.WHITE,
4046 }.get(status, Fore.WHITE)
4047
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004048
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004049def get_cl_statuses(changes, fine_grained, max_processes=None):
4050 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004051
4052 If fine_grained is true, this will fetch CL statuses from the server.
4053 Otherwise, simply indicate if there's a matching url for the given branches.
4054
4055 If max_processes is specified, it is used as the maximum number of processes
4056 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4057 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004058
4059 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004060 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004061 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004062 upload.verbosity = 0
4063
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004064 if not changes:
4065 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004066
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004067 if not fine_grained:
4068 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004069 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004070 for cl in changes:
4071 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004072 return
4073
4074 # First, sort out authentication issues.
4075 logging.debug('ensuring credentials exist')
4076 for cl in changes:
4077 cl.EnsureAuthenticated(force=False, refresh=True)
4078
4079 def fetch(cl):
4080 try:
4081 return (cl, cl.GetStatus())
4082 except:
4083 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004084 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004085 raise
4086
4087 threads_count = len(changes)
4088 if max_processes:
4089 threads_count = max(1, min(threads_count, max_processes))
4090 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4091
4092 pool = ThreadPool(threads_count)
4093 fetched_cls = set()
4094 try:
4095 it = pool.imap_unordered(fetch, changes).__iter__()
4096 while True:
4097 try:
4098 cl, status = it.next(timeout=5)
4099 except multiprocessing.TimeoutError:
4100 break
4101 fetched_cls.add(cl)
4102 yield cl, status
4103 finally:
4104 pool.close()
4105
4106 # Add any branches that failed to fetch.
4107 for cl in set(changes) - fetched_cls:
4108 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004109
rmistry@google.com2dd99862015-06-22 12:22:18 +00004110
4111def upload_branch_deps(cl, args):
4112 """Uploads CLs of local branches that are dependents of the current branch.
4113
4114 If the local branch dependency tree looks like:
4115 test1 -> test2.1 -> test3.1
4116 -> test3.2
4117 -> test2.2 -> test3.3
4118
4119 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4120 run on the dependent branches in this order:
4121 test2.1, test3.1, test3.2, test2.2, test3.3
4122
4123 Note: This function does not rebase your local dependent branches. Use it when
4124 you make a change to the parent branch that will not conflict with its
4125 dependent branches, and you would like their dependencies updated in
4126 Rietveld.
4127 """
4128 if git_common.is_dirty_git_tree('upload-branch-deps'):
4129 return 1
4130
4131 root_branch = cl.GetBranch()
4132 if root_branch is None:
4133 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4134 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004135 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00004136 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4137 'patchset dependencies without an uploaded CL.')
4138
4139 branches = RunGit(['for-each-ref',
4140 '--format=%(refname:short) %(upstream:short)',
4141 'refs/heads'])
4142 if not branches:
4143 print('No local branches found.')
4144 return 0
4145
4146 # Create a dictionary of all local branches to the branches that are dependent
4147 # on it.
4148 tracked_to_dependents = collections.defaultdict(list)
4149 for b in branches.splitlines():
4150 tokens = b.split()
4151 if len(tokens) == 2:
4152 branch_name, tracked = tokens
4153 tracked_to_dependents[tracked].append(branch_name)
4154
vapiera7fbd5a2016-06-16 09:17:49 -07004155 print()
4156 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004157 dependents = []
4158 def traverse_dependents_preorder(branch, padding=''):
4159 dependents_to_process = tracked_to_dependents.get(branch, [])
4160 padding += ' '
4161 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004162 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004163 dependents.append(dependent)
4164 traverse_dependents_preorder(dependent, padding)
4165 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004166 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004167
4168 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004169 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004170 return 0
4171
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004172 confirm_or_exit('This command will checkout all dependent branches and run '
4173 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004174
rmistry@google.com2dd99862015-06-22 12:22:18 +00004175 # Record all dependents that failed to upload.
4176 failures = {}
4177 # Go through all dependents, checkout the branch and upload.
4178 try:
4179 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004180 print()
4181 print('--------------------------------------')
4182 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004183 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004184 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004185 try:
4186 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004187 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004188 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004189 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004190 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004191 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004192 finally:
4193 # Swap back to the original root branch.
4194 RunGit(['checkout', '-q', root_branch])
4195
vapiera7fbd5a2016-06-16 09:17:49 -07004196 print()
4197 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004198 for dependent_branch in dependents:
4199 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004200 print(' %s : %s' % (dependent_branch, upload_status))
4201 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004202
4203 return 0
4204
4205
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004206@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004207def CMDarchive(parser, args):
4208 """Archives and deletes branches associated with closed changelists."""
4209 parser.add_option(
4210 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004211 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004212 parser.add_option(
4213 '-f', '--force', action='store_true',
4214 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004215 parser.add_option(
4216 '-d', '--dry-run', action='store_true',
4217 help='Skip the branch tagging and removal steps.')
4218 parser.add_option(
4219 '-t', '--notags', action='store_true',
4220 help='Do not tag archived branches. '
4221 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004222
4223 auth.add_auth_options(parser)
4224 options, args = parser.parse_args(args)
4225 if args:
4226 parser.error('Unsupported args: %s' % ' '.join(args))
4227 auth_config = auth.extract_auth_config_from_options(options)
4228
4229 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4230 if not branches:
4231 return 0
4232
vapiera7fbd5a2016-06-16 09:17:49 -07004233 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004234 changes = [Changelist(branchref=b, auth_config=auth_config)
4235 for b in branches.splitlines()]
4236 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4237 statuses = get_cl_statuses(changes,
4238 fine_grained=True,
4239 max_processes=options.maxjobs)
4240 proposal = [(cl.GetBranch(),
4241 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4242 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00004243 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07004244 proposal.sort()
4245
4246 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004247 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004248 return 0
4249
4250 current_branch = GetCurrentBranch()
4251
vapiera7fbd5a2016-06-16 09:17:49 -07004252 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004253 if options.notags:
4254 for next_item in proposal:
4255 print(' ' + next_item[0])
4256 else:
4257 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4258 for next_item in proposal:
4259 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004260
kmarshall9249e012016-08-23 12:02:16 -07004261 # Quit now on precondition failure or if instructed by the user, either
4262 # via an interactive prompt or by command line flags.
4263 if options.dry_run:
4264 print('\nNo changes were made (dry run).\n')
4265 return 0
4266 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004267 print('You are currently on a branch \'%s\' which is associated with a '
4268 'closed codereview issue, so archive cannot proceed. Please '
4269 'checkout another branch and run this command again.' %
4270 current_branch)
4271 return 1
kmarshall9249e012016-08-23 12:02:16 -07004272 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004273 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4274 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004275 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004276 return 1
4277
4278 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004279 if not options.notags:
4280 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004281 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004282
vapiera7fbd5a2016-06-16 09:17:49 -07004283 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004284
4285 return 0
4286
4287
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004288@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004289def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004290 """Show status of changelists.
4291
4292 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004293 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004294 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004295 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004296 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004297 - Magenta in the commit queue
4298 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004299 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004300
4301 Also see 'git cl comments'.
4302 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004303 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004304 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004305 parser.add_option('-f', '--fast', action='store_true',
4306 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004307 parser.add_option(
4308 '-j', '--maxjobs', action='store', type=int,
4309 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004310
4311 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004312 _add_codereview_issue_select_options(
4313 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004314 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004315 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004316 if args:
4317 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004318 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004319
iannuccie53c9352016-08-17 14:40:40 -07004320 if options.issue is not None and not options.field:
4321 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004322
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004323 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004324 cl = Changelist(auth_config=auth_config, issue=options.issue,
4325 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004326 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004327 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004328 elif options.field == 'id':
4329 issueid = cl.GetIssue()
4330 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004331 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004332 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004333 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004334 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004335 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004336 elif options.field == 'status':
4337 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004338 elif options.field == 'url':
4339 url = cl.GetIssueURL()
4340 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004341 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004342 return 0
4343
4344 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4345 if not branches:
4346 print('No local branch found.')
4347 return 0
4348
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004349 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004350 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004351 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004352 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004353 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004354 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004355 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004356
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004357 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004358 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4359 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4360 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004361 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004362 c, status = output.next()
4363 branch_statuses[c.GetBranch()] = status
4364 status = branch_statuses.pop(branch)
4365 url = cl.GetIssueURL()
4366 if url and (not status or status == 'error'):
4367 # The issue probably doesn't exist anymore.
4368 url += ' (broken)'
4369
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004370 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004371 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004372 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004373 color = ''
4374 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004375 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004376 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004377 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004378 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004379
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004380
4381 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004382 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004383 print('Current branch: %s' % branch)
4384 for cl in changes:
4385 if cl.GetBranch() == branch:
4386 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004387 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004388 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004389 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004390 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004391 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004392 print('Issue description:')
4393 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004394 return 0
4395
4396
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004397def colorize_CMDstatus_doc():
4398 """To be called once in main() to add colors to git cl status help."""
4399 colors = [i for i in dir(Fore) if i[0].isupper()]
4400
4401 def colorize_line(line):
4402 for color in colors:
4403 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004404 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004405 indent = len(line) - len(line.lstrip(' ')) + 1
4406 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4407 return line
4408
4409 lines = CMDstatus.__doc__.splitlines()
4410 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4411
4412
phajdan.jre328cf92016-08-22 04:12:17 -07004413def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004414 if path == '-':
4415 json.dump(contents, sys.stdout)
4416 else:
4417 with open(path, 'w') as f:
4418 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004419
4420
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004421@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004422@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004423def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004424 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004425
4426 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004427 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004428 parser.add_option('-r', '--reverse', action='store_true',
4429 help='Lookup the branch(es) for the specified issues. If '
4430 'no issues are specified, all branches with mapped '
4431 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004432 parser.add_option('--json',
4433 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004434 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004435 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004436 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004437
dnj@chromium.org406c4402015-03-03 17:22:28 +00004438 if options.reverse:
4439 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004440 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004441 # Reverse issue lookup.
4442 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004443
4444 git_config = {}
4445 for config in RunGit(['config', '--get-regexp',
4446 r'branch\..*issue']).splitlines():
4447 name, _space, val = config.partition(' ')
4448 git_config[name] = val
4449
dnj@chromium.org406c4402015-03-03 17:22:28 +00004450 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004451 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4452 config_key = _git_branch_config_key(ShortBranchName(branch),
4453 cls.IssueConfigKey())
4454 issue = git_config.get(config_key)
4455 if issue:
4456 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004457 if not args:
4458 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004459 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004460 for issue in args:
4461 if not issue:
4462 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004463 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004464 print('Branch for issue number %s: %s' % (
4465 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004466 if options.json:
4467 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004468 return 0
4469
4470 if len(args) > 0:
4471 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4472 if not issue.valid:
4473 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4474 'or no argument to list it.\n'
4475 'Maybe you want to run git cl status?')
4476 cl = Changelist(codereview=issue.codereview)
4477 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004478 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004479 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004480 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4481 if options.json:
4482 write_json(options.json, {
4483 'issue': cl.GetIssue(),
4484 'issue_url': cl.GetIssueURL(),
4485 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004486 return 0
4487
4488
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004489@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004490def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004491 """Shows or posts review comments for any changelist."""
4492 parser.add_option('-a', '--add-comment', dest='comment',
4493 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004494 parser.add_option('-p', '--publish', action='store_true',
4495 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004496 parser.add_option('-i', '--issue', dest='issue',
4497 help='review issue id (defaults to current issue). '
4498 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004499 parser.add_option('-m', '--machine-readable', dest='readable',
4500 action='store_false', default=True,
4501 help='output comments in a format compatible with '
4502 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004503 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004504 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004505 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004506 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004507 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004508 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004509 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004510
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004511 issue = None
4512 if options.issue:
4513 try:
4514 issue = int(options.issue)
4515 except ValueError:
4516 DieWithError('A review issue id is expected to be a number')
4517
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004518 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4519
4520 if not cl.IsGerrit():
4521 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004522
4523 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004524 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004525 return 0
4526
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004527 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4528 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004529 for comment in summary:
4530 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004531 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004532 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004533 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004534 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004535 color = Fore.MAGENTA
4536 else:
4537 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004538 print('\n%s%s %s%s\n%s' % (
4539 color,
4540 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4541 comment.sender,
4542 Fore.RESET,
4543 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4544
smut@google.comc85ac942015-09-15 16:34:43 +00004545 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004546 def pre_serialize(c):
4547 dct = c.__dict__.copy()
4548 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4549 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004550 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004551 return 0
4552
4553
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004554@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004555@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004556def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004557 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004558 parser.add_option('-d', '--display', action='store_true',
4559 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004560 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004561 help='New description to set for this issue (- for stdin, '
4562 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004563 parser.add_option('-f', '--force', action='store_true',
4564 help='Delete any unpublished Gerrit edits for this issue '
4565 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004566
4567 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004568 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004569 options, args = parser.parse_args(args)
4570 _process_codereview_select_options(parser, options)
4571
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004572 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004573 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004574 target_issue_arg = ParseIssueNumberArgument(args[0],
4575 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004576 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004577 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004578
martiniss6eda05f2016-06-30 10:18:35 -07004579 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004580 'auth_config': auth.extract_auth_config_from_options(options),
4581 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004582 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004583 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004584 if target_issue_arg:
4585 kwargs['issue'] = target_issue_arg.issue
4586 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004587 if target_issue_arg.codereview and not options.forced_codereview:
4588 detected_codereview_from_url = True
4589 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004590
4591 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004592 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004593 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004594 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004595
4596 if detected_codereview_from_url:
4597 logging.info('canonical issue/change URL: %s (type: %s)\n',
4598 cl.GetIssueURL(), target_issue_arg.codereview)
4599
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004600 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004601
smut@google.com34fb6b12015-07-13 20:03:26 +00004602 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004603 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004604 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004605
4606 if options.new_description:
4607 text = options.new_description
4608 if text == '-':
4609 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004610 elif text == '+':
4611 base_branch = cl.GetCommonAncestorWithUpstream()
4612 change = cl.GetChange(base_branch, None, local_description=True)
4613 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004614
4615 description.set_description(text)
4616 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004617 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004618
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004619 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004620 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004621 return 0
4622
4623
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004624@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004625def CMDlint(parser, args):
4626 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004627 parser.add_option('--filter', action='append', metavar='-x,+y',
4628 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004629 auth.add_auth_options(parser)
4630 options, args = parser.parse_args(args)
4631 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004632
4633 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004634 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004635 try:
4636 import cpplint
4637 import cpplint_chromium
4638 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004639 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004640 return 1
4641
4642 # Change the current working directory before calling lint so that it
4643 # shows the correct base.
4644 previous_cwd = os.getcwd()
4645 os.chdir(settings.GetRoot())
4646 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004647 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004648 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4649 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004650 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004651 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004652 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004653
4654 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004655 command = args + files
4656 if options.filter:
4657 command = ['--filter=' + ','.join(options.filter)] + command
4658 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004659
4660 white_regex = re.compile(settings.GetLintRegex())
4661 black_regex = re.compile(settings.GetLintIgnoreRegex())
4662 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4663 for filename in filenames:
4664 if white_regex.match(filename):
4665 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004666 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004667 else:
4668 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4669 extra_check_functions)
4670 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004671 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004672 finally:
4673 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004674 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004675 if cpplint._cpplint_state.error_count != 0:
4676 return 1
4677 return 0
4678
4679
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004680@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004681def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004682 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004683 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004684 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004685 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004686 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004687 parser.add_option('--all', action='store_true',
4688 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004689 parser.add_option('--parallel', action='store_true',
4690 help='Run all tests specified by input_api.RunTests in all '
4691 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004692 auth.add_auth_options(parser)
4693 options, args = parser.parse_args(args)
4694 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004695
sbc@chromium.org71437c02015-04-09 19:29:40 +00004696 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004697 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004698 return 1
4699
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004700 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004701 if args:
4702 base_branch = args[0]
4703 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004704 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004705 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004706
Aaron Gable8076c282017-11-29 14:39:41 -08004707 if options.all:
4708 base_change = cl.GetChange(base_branch, None)
4709 files = [('M', f) for f in base_change.AllFiles()]
4710 change = presubmit_support.GitChange(
4711 base_change.Name(),
4712 base_change.FullDescriptionText(),
4713 base_change.RepositoryRoot(),
4714 files,
4715 base_change.issue,
4716 base_change.patchset,
4717 base_change.author_email,
4718 base_change._upstream)
4719 else:
4720 change = cl.GetChange(base_branch, None)
4721
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004722 cl.RunHook(
4723 committing=not options.upload,
4724 may_prompt=False,
4725 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004726 change=change,
4727 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004728 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004729
4730
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004731def GenerateGerritChangeId(message):
4732 """Returns Ixxxxxx...xxx change id.
4733
4734 Works the same way as
4735 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4736 but can be called on demand on all platforms.
4737
4738 The basic idea is to generate git hash of a state of the tree, original commit
4739 message, author/committer info and timestamps.
4740 """
4741 lines = []
4742 tree_hash = RunGitSilent(['write-tree'])
4743 lines.append('tree %s' % tree_hash.strip())
4744 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4745 if code == 0:
4746 lines.append('parent %s' % parent.strip())
4747 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4748 lines.append('author %s' % author.strip())
4749 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4750 lines.append('committer %s' % committer.strip())
4751 lines.append('')
4752 # Note: Gerrit's commit-hook actually cleans message of some lines and
4753 # whitespace. This code is not doing this, but it clearly won't decrease
4754 # entropy.
4755 lines.append(message)
4756 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4757 stdin='\n'.join(lines))
4758 return 'I%s' % change_hash.strip()
4759
4760
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004761def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004762 """Computes the remote branch ref to use for the CL.
4763
4764 Args:
4765 remote (str): The git remote for the CL.
4766 remote_branch (str): The git remote branch for the CL.
4767 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004768 """
4769 if not (remote and remote_branch):
4770 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004771
wittman@chromium.org455dc922015-01-26 20:15:50 +00004772 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004773 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004774 # refs, which are then translated into the remote full symbolic refs
4775 # below.
4776 if '/' not in target_branch:
4777 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4778 else:
4779 prefix_replacements = (
4780 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4781 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4782 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4783 )
4784 match = None
4785 for regex, replacement in prefix_replacements:
4786 match = re.search(regex, target_branch)
4787 if match:
4788 remote_branch = target_branch.replace(match.group(0), replacement)
4789 break
4790 if not match:
4791 # This is a branch path but not one we recognize; use as-is.
4792 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004793 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4794 # Handle the refs that need to land in different refs.
4795 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004796
wittman@chromium.org455dc922015-01-26 20:15:50 +00004797 # Create the true path to the remote branch.
4798 # Does the following translation:
4799 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4800 # * refs/remotes/origin/master -> refs/heads/master
4801 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4802 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4803 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4804 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4805 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4806 'refs/heads/')
4807 elif remote_branch.startswith('refs/remotes/branch-heads'):
4808 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004809
wittman@chromium.org455dc922015-01-26 20:15:50 +00004810 return remote_branch
4811
4812
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004813def cleanup_list(l):
4814 """Fixes a list so that comma separated items are put as individual items.
4815
4816 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4817 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4818 """
4819 items = sum((i.split(',') for i in l), [])
4820 stripped_items = (i.strip() for i in items)
4821 return sorted(filter(None, stripped_items))
4822
4823
Aaron Gable4db38df2017-11-03 14:59:07 -07004824@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004825@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004826def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004827 """Uploads the current changelist to codereview.
4828
4829 Can skip dependency patchset uploads for a branch by running:
4830 git config branch.branch_name.skip-deps-uploads True
4831 To unset run:
4832 git config --unset branch.branch_name.skip-deps-uploads
4833 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004834
4835 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4836 a bug number, this bug number is automatically populated in the CL
4837 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004838
4839 If subject contains text in square brackets or has "<text>: " prefix, such
4840 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4841 [git-cl] add support for hashtags
4842 Foo bar: implement foo
4843 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004844 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004845 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4846 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004847 parser.add_option('--bypass-watchlists', action='store_true',
4848 dest='bypass_watchlists',
4849 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004850 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004851 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004852 parser.add_option('--message', '-m', dest='message',
4853 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004854 parser.add_option('-b', '--bug',
4855 help='pre-populate the bug number(s) for this issue. '
4856 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004857 parser.add_option('--message-file', dest='message_file',
4858 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004859 parser.add_option('--title', '-t', dest='title',
4860 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004861 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004862 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004863 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004864 parser.add_option('--tbrs',
4865 action='append', default=[],
4866 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004867 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004868 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004869 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004870 parser.add_option('--hashtag', dest='hashtags',
4871 action='append', default=[],
4872 help=('Gerrit hashtag for new CL; '
4873 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004874 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004875 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004876 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004877 help='tell the commit queue to commit this patchset; '
4878 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004879 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004880 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004881 metavar='TARGET',
4882 help='Apply CL to remote ref TARGET. ' +
4883 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004884 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004885 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004886 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004887 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004888 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004889 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004890 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4891 const='TBR', help='add a set of OWNERS to TBR')
4892 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4893 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004894 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4895 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004896 help='Send the patchset to do a CQ dry run right after '
4897 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004898 parser.add_option('--dependencies', action='store_true',
4899 help='Uploads CLs of all the local branches that depend on '
4900 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004901 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4902 help='Sends your change to the CQ after an approval. Only '
4903 'works on repos that have the Auto-Submit label '
4904 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004905 parser.add_option('--parallel', action='store_true',
4906 help='Run all tests specified by input_api.RunTests in all '
4907 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004908
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004909 parser.add_option('--no-autocc', action='store_true',
4910 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004911 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004912 help='Set the review private. This implies --no-autocc.')
4913
rmistry@google.com2dd99862015-06-22 12:22:18 +00004914 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004915 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004916 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004917 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004918 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004919 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004920
sbc@chromium.org71437c02015-04-09 19:29:40 +00004921 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004922 return 1
4923
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004924 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004925 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004926 options.cc = cleanup_list(options.cc)
4927
tandriib80458a2016-06-23 12:20:07 -07004928 if options.message_file:
4929 if options.message:
4930 parser.error('only one of --message and --message-file allowed.')
4931 options.message = gclient_utils.FileRead(options.message_file)
4932 options.message_file = None
4933
tandrii4d0545a2016-07-06 03:56:49 -07004934 if options.cq_dry_run and options.use_commit_queue:
4935 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4936
Aaron Gableedbc4132017-09-11 13:22:28 -07004937 if options.use_commit_queue:
4938 options.send_mail = True
4939
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004940 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4941 settings.GetIsGerrit()
4942
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004943 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004944 if not cl.IsGerrit():
4945 # Error out with instructions for repos not yet configured for Gerrit.
4946 print('=====================================')
4947 print('NOTICE: Rietveld is no longer supported. '
4948 'You can upload changes to Gerrit with')
4949 print(' git cl upload --gerrit')
4950 print('or set Gerrit to be your default code review tool with')
4951 print(' git config gerrit.host true')
4952 print('=====================================')
4953 return 1
4954
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004955 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004956
4957
Francois Dorayd42c6812017-05-30 15:10:20 -04004958@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004959@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004960def CMDsplit(parser, args):
4961 """Splits a branch into smaller branches and uploads CLs.
4962
4963 Creates a branch and uploads a CL for each group of files modified in the
4964 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004965 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004966 the shared OWNERS file.
4967 """
4968 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004969 help="A text file containing a CL description in which "
4970 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004971 parser.add_option("-c", "--comment", dest="comment_file",
4972 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004973 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4974 default=False,
4975 help="List the files and reviewers for each CL that would "
4976 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004977 parser.add_option("--cq-dry-run", action='store_true',
4978 help="If set, will do a cq dry run for each uploaded CL. "
4979 "Please be careful when doing this; more than ~10 CLs "
4980 "has the potential to overload our build "
4981 "infrastructure. Try to upload these not during high "
4982 "load times (usually 11-3 Mountain View time). Email "
4983 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004984 options, _ = parser.parse_args(args)
4985
4986 if not options.description_file:
4987 parser.error('No --description flag specified.')
4988
4989 def WrappedCMDupload(args):
4990 return CMDupload(OptionParser(), args)
4991
4992 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004993 Changelist, WrappedCMDupload, options.dry_run,
4994 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04004995
4996
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004997@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004998@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004999def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005000 """DEPRECATED: Used to commit the current changelist via git-svn."""
5001 message = ('git-cl no longer supports committing to SVN repositories via '
5002 'git-svn. You probably want to use `git cl land` instead.')
5003 print(message)
5004 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005005
5006
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005007# Two special branches used by git cl land.
5008MERGE_BRANCH = 'git-cl-commit'
5009CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5010
5011
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005012@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005013@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005014def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005015 """Commits the current changelist via git.
5016
5017 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5018 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005019 """
5020 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5021 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07005022 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005023 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005024 parser.add_option('--parallel', action='store_true',
5025 help='Run all tests specified by input_api.RunTests in all '
5026 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005027 auth.add_auth_options(parser)
5028 (options, args) = parser.parse_args(args)
5029 auth_config = auth.extract_auth_config_from_options(options)
5030
5031 cl = Changelist(auth_config=auth_config)
5032
Robert Iannucci2e73d432018-03-14 01:10:47 -07005033 if not cl.IsGerrit():
5034 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005035
Robert Iannucci2e73d432018-03-14 01:10:47 -07005036 if not cl.GetIssue():
5037 DieWithError('You must upload the change first to Gerrit.\n'
5038 ' If you would rather have `git cl land` upload '
5039 'automatically for you, see http://crbug.com/642759')
5040 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005041 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005042
5043
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005044@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005045@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005046def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005047 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005048 parser.add_option('-b', dest='newbranch',
5049 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005050 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005051 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005052 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005053 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005054 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005055 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005056 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005057 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005058 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005059 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005060
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005061
5062 group = optparse.OptionGroup(
5063 parser,
5064 'Options for continuing work on the current issue uploaded from a '
5065 'different clone (e.g. different machine). Must be used independently '
5066 'from the other options. No issue number should be specified, and the '
5067 'branch must have an issue number associated with it')
5068 group.add_option('--reapply', action='store_true', dest='reapply',
5069 help='Reset the branch and reapply the issue.\n'
5070 'CAUTION: This will undo any local changes in this '
5071 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005072
5073 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005074 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005075 parser.add_option_group(group)
5076
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005077 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005078 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005079 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005080 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005081 auth_config = auth.extract_auth_config_from_options(options)
5082
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005083 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005084 if options.newbranch:
5085 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005086 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005087 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005088
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005089 cl = Changelist(auth_config=auth_config,
5090 codereview=options.forced_codereview)
5091 if not cl.GetIssue():
5092 parser.error('current branch must have an associated issue')
5093
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005094 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005095 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005096 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005097
5098 RunGit(['reset', '--hard', upstream])
5099 if options.pull:
5100 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005101
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005102 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5103 options.directory)
5104
5105 if len(args) != 1 or not args[0]:
5106 parser.error('Must specify issue number or url')
5107
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005108 target_issue_arg = ParseIssueNumberArgument(args[0],
5109 options.forced_codereview)
5110 if not target_issue_arg.valid:
5111 parser.error('invalid codereview url or CL id')
5112
5113 cl_kwargs = {
5114 'auth_config': auth_config,
5115 'codereview_host': target_issue_arg.hostname,
5116 'codereview': options.forced_codereview,
5117 }
5118 detected_codereview_from_url = False
5119 if target_issue_arg.codereview and not options.forced_codereview:
5120 detected_codereview_from_url = True
5121 cl_kwargs['codereview'] = target_issue_arg.codereview
5122 cl_kwargs['issue'] = target_issue_arg.issue
5123
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005124 # We don't want uncommitted changes mixed up with the patch.
5125 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005126 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005127
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005128 if options.newbranch:
5129 if options.force:
5130 RunGit(['branch', '-D', options.newbranch],
5131 stderr=subprocess2.PIPE, error_ok=True)
5132 RunGit(['new-branch', options.newbranch])
5133
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005134 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005135
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005136 if cl.IsGerrit():
5137 if options.reject:
5138 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005139 if options.directory:
5140 parser.error('--directory is not supported with Gerrit codereview.')
5141
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005142 if detected_codereview_from_url:
5143 print('canonical issue/change URL: %s (type: %s)\n' %
5144 (cl.GetIssueURL(), target_issue_arg.codereview))
5145
5146 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005147 options.nocommit, options.directory,
5148 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005149
5150
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005151def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005152 """Fetches the tree status and returns either 'open', 'closed',
5153 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005154 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005155 if url:
5156 status = urllib2.urlopen(url).read().lower()
5157 if status.find('closed') != -1 or status == '0':
5158 return 'closed'
5159 elif status.find('open') != -1 or status == '1':
5160 return 'open'
5161 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005162 return 'unset'
5163
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005164
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005165def GetTreeStatusReason():
5166 """Fetches the tree status from a json url and returns the message
5167 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005168 url = settings.GetTreeStatusUrl()
5169 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005170 connection = urllib2.urlopen(json_url)
5171 status = json.loads(connection.read())
5172 connection.close()
5173 return status['message']
5174
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005175
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005176@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005177def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005178 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005179 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005180 status = GetTreeStatus()
5181 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005182 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005183 return 2
5184
vapiera7fbd5a2016-06-16 09:17:49 -07005185 print('The tree is %s' % status)
5186 print()
5187 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005188 if status != 'open':
5189 return 1
5190 return 0
5191
5192
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005193@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005194def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005195 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005196 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005197 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005198 '-b', '--bot', action='append',
5199 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5200 'times to specify multiple builders. ex: '
5201 '"-b win_rel -b win_layout". See '
5202 'the try server waterfall for the builders name and the tests '
5203 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005204 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005205 '-B', '--bucket', default='',
5206 help=('Buildbucket bucket to send the try requests.'))
5207 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005208 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005209 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005210 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005211 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005212 help='Revision to use for the try job; default: the revision will '
5213 'be determined by the try recipe that builder runs, which usually '
5214 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005215 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005216 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005217 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005218 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005219 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005220 '--category', default='git_cl_try', help='Specify custom build category.')
5221 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005222 '--project',
5223 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005224 'in recipe to determine to which repository or directory to '
5225 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005226 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005227 '-p', '--property', dest='properties', action='append', default=[],
5228 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005229 'key2=value2 etc. The value will be treated as '
5230 'json if decodable, or as string otherwise. '
5231 'NOTE: using this may make your try job not usable for CQ, '
5232 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005233 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005234 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5235 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005236 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005237 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005238 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005239 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005240 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005241 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005242
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005243 if options.master and options.master.startswith('luci.'):
5244 parser.error(
5245 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005246 # Make sure that all properties are prop=value pairs.
5247 bad_params = [x for x in options.properties if '=' not in x]
5248 if bad_params:
5249 parser.error('Got properties with missing "=": %s' % bad_params)
5250
maruel@chromium.org15192402012-09-06 12:38:29 +00005251 if args:
5252 parser.error('Unknown arguments: %s' % args)
5253
Koji Ishii31c14782018-01-08 17:17:33 +09005254 cl = Changelist(auth_config=auth_config, issue=options.issue,
5255 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005256 if not cl.GetIssue():
5257 parser.error('Need to upload first')
5258
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005259 if cl.IsGerrit():
5260 # HACK: warm up Gerrit change detail cache to save on RPCs.
5261 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5262
tandriie113dfd2016-10-11 10:20:12 -07005263 error_message = cl.CannotTriggerTryJobReason()
5264 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005265 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005266
borenet6c0efe62016-10-19 08:13:29 -07005267 if options.bucket and options.master:
5268 parser.error('Only one of --bucket and --master may be used.')
5269
qyearsley1fdfcb62016-10-24 13:22:03 -07005270 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005271
qyearsleydd49f942016-10-28 11:57:22 -07005272 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5273 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005274 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005275 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005276 print('git cl try with no bots now defaults to CQ dry run.')
5277 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5278 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005279
borenet6c0efe62016-10-19 08:13:29 -07005280 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005281 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005282 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005283 'of bot requires an initial job from a parent (usually a builder). '
5284 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005285 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005286 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005287
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005288 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005289 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005290 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005291 except BuildbucketResponseException as ex:
5292 print('ERROR: %s' % ex)
5293 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005294 return 0
5295
5296
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005297@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005298def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005299 """Prints info about try jobs associated with current CL."""
5300 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005301 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005302 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005303 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005304 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005305 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005306 '--color', action='store_true', default=setup_color.IS_TTY,
5307 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005308 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005309 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5310 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005311 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005312 '--json', help=('Path of JSON output file to write try job results to,'
5313 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005314 parser.add_option_group(group)
5315 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005316 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005317 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005318 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005319 if args:
5320 parser.error('Unrecognized args: %s' % ' '.join(args))
5321
5322 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005323 cl = Changelist(
5324 issue=options.issue, codereview=options.forced_codereview,
5325 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005326 if not cl.GetIssue():
5327 parser.error('Need to upload first')
5328
tandrii221ab252016-10-06 08:12:04 -07005329 patchset = options.patchset
5330 if not patchset:
5331 patchset = cl.GetMostRecentPatchset()
5332 if not patchset:
5333 parser.error('Codereview doesn\'t know about issue %s. '
5334 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005335 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005336 cl.GetIssue())
5337
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005338 try:
tandrii221ab252016-10-06 08:12:04 -07005339 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005340 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005341 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005342 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005343 if options.json:
5344 write_try_results_json(options.json, jobs)
5345 else:
5346 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005347 return 0
5348
5349
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005350@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005351@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005352def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005353 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005354 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005355 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005356 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005357
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005358 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005359 if args:
5360 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005361 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005362 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005363 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005364 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005365
5366 # Clear configured merge-base, if there is one.
5367 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005368 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005369 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005370 return 0
5371
5372
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005373@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005374def CMDweb(parser, args):
5375 """Opens the current CL in the web browser."""
5376 _, args = parser.parse_args(args)
5377 if args:
5378 parser.error('Unrecognized args: %s' % ' '.join(args))
5379
5380 issue_url = Changelist().GetIssueURL()
5381 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005382 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005383 return 1
5384
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005385 # Redirect I/O before invoking browser to hide its output. For example, this
5386 # allows to hide "Created new window in existing browser session." message
5387 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5388 saved_stdout = os.dup(1)
5389 os.close(1)
5390 os.open(os.devnull, os.O_RDWR)
5391 try:
5392 webbrowser.open(issue_url)
5393 finally:
5394 os.dup2(saved_stdout, 1)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005395 return 0
5396
5397
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005398@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005399def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005400 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005401 parser.add_option('-d', '--dry-run', action='store_true',
5402 help='trigger in dry run mode')
5403 parser.add_option('-c', '--clear', action='store_true',
5404 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005405 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005406 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005407 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005408 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005409 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005410 if args:
5411 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005412 if options.dry_run and options.clear:
5413 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5414
iannuccie53c9352016-08-17 14:40:40 -07005415 cl = Changelist(auth_config=auth_config, issue=options.issue,
5416 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005417 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005418 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005419 elif options.dry_run:
5420 state = _CQState.DRY_RUN
5421 else:
5422 state = _CQState.COMMIT
5423 if not cl.GetIssue():
5424 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005425 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005426 return 0
5427
5428
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005429@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005430def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005431 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005432 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005433 auth.add_auth_options(parser)
5434 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005435 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005436 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005437 if args:
5438 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005439 cl = Changelist(auth_config=auth_config, issue=options.issue,
5440 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005441 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005442 if not cl.GetIssue():
5443 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005444 cl.CloseIssue()
5445 return 0
5446
5447
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005448@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005449def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005450 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005451 parser.add_option(
5452 '--stat',
5453 action='store_true',
5454 dest='stat',
5455 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005456 auth.add_auth_options(parser)
5457 options, args = parser.parse_args(args)
5458 auth_config = auth.extract_auth_config_from_options(options)
5459 if args:
5460 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005461
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005462 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005463 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005464 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005465 if not issue:
5466 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005467
Aaron Gablea718c3e2017-08-28 17:47:28 -07005468 base = cl._GitGetBranchConfigValue('last-upload-hash')
5469 if not base:
5470 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5471 if not base:
5472 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5473 revision_info = detail['revisions'][detail['current_revision']]
5474 fetch_info = revision_info['fetch']['http']
5475 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5476 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005477
Aaron Gablea718c3e2017-08-28 17:47:28 -07005478 cmd = ['git', 'diff']
5479 if options.stat:
5480 cmd.append('--stat')
5481 cmd.append(base)
5482 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005483
5484 return 0
5485
5486
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005487@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005488def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005489 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005490 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005491 '--ignore-current',
5492 action='store_true',
5493 help='Ignore the CL\'s current reviewers and start from scratch.')
5494 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005495 '--no-color',
5496 action='store_true',
5497 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005498 parser.add_option(
5499 '--batch',
5500 action='store_true',
5501 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005502 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005503 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005504 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005505
5506 author = RunGit(['config', 'user.email']).strip() or None
5507
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005508 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005509
5510 if args:
5511 if len(args) > 1:
5512 parser.error('Unknown args')
5513 base_branch = args[0]
5514 else:
5515 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005516 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005517
5518 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005519 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5520
5521 if options.batch:
5522 db = owners.Database(change.RepositoryRoot(), file, os.path)
5523 print('\n'.join(db.reviewers_for(affected_files, author)))
5524 return 0
5525
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005526 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005527 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005528 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005529 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005530 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005531 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005532 disable_color=options.no_color,
5533 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005534
5535
Aiden Bennerc08566e2018-10-03 17:52:42 +00005536def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005537 """Generates a diff command."""
5538 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005539 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5540
5541 if not allow_prefix:
5542 diff_cmd += ['--no-prefix']
5543
5544 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005545
5546 if args:
5547 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005548 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005549 diff_cmd.append(arg)
5550 else:
5551 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005552
5553 return diff_cmd
5554
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005555
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005556def MatchingFileType(file_name, extensions):
5557 """Returns true if the file name ends with one of the given extensions."""
5558 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005559
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005560
enne@chromium.org555cfe42014-01-29 18:21:39 +00005561@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005562@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005563def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005564 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005565 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005566 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005567 parser.add_option('--full', action='store_true',
5568 help='Reformat the full content of all touched files')
5569 parser.add_option('--dry-run', action='store_true',
5570 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005571 parser.add_option('--python', action='store_true',
5572 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005573 parser.add_option('--js', action='store_true',
5574 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005575 parser.add_option('--diff', action='store_true',
5576 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005577 parser.add_option('--presubmit', action='store_true',
5578 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005579 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005580
Daniel Chengc55eecf2016-12-30 03:11:02 -08005581 # Normalize any remaining args against the current path, so paths relative to
5582 # the current directory are still resolved as expected.
5583 args = [os.path.join(os.getcwd(), arg) for arg in args]
5584
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005585 # git diff generates paths against the root of the repository. Change
5586 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005587 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005588 if rel_base_path:
5589 os.chdir(rel_base_path)
5590
digit@chromium.org29e47272013-05-17 17:01:46 +00005591 # Grab the merge-base commit, i.e. the upstream commit of the current
5592 # branch when it was created or the last time it was rebased. This is
5593 # to cover the case where the user may have called "git fetch origin",
5594 # moving the origin branch to a newer commit, but hasn't rebased yet.
5595 upstream_commit = None
5596 cl = Changelist()
5597 upstream_branch = cl.GetUpstreamBranch()
5598 if upstream_branch:
5599 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5600 upstream_commit = upstream_commit.strip()
5601
5602 if not upstream_commit:
5603 DieWithError('Could not find base commit for this branch. '
5604 'Are you in detached state?')
5605
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005606 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5607 diff_output = RunGit(changed_files_cmd)
5608 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005609 # Filter out files deleted by this CL
5610 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005611
Christopher Lamc5ba6922017-01-24 11:19:14 +11005612 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005613 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005614
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005615 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5616 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5617 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005618 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005619
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005620 top_dir = os.path.normpath(
5621 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5622
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005623 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5624 # formatted. This is used to block during the presubmit.
5625 return_value = 0
5626
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005627 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005628 # Locate the clang-format binary in the checkout
5629 try:
5630 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005631 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005632 DieWithError(e)
5633
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005634 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005635 cmd = [clang_format_tool]
5636 if not opts.dry_run and not opts.diff:
5637 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005638 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005639 if opts.diff:
5640 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005641 else:
5642 env = os.environ.copy()
5643 env['PATH'] = str(os.path.dirname(clang_format_tool))
5644 try:
5645 script = clang_format.FindClangFormatScriptInChromiumTree(
5646 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005647 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005648 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005649
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005650 cmd = [sys.executable, script, '-p0']
5651 if not opts.dry_run and not opts.diff:
5652 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005653
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005654 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5655 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005656
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005657 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5658 if opts.diff:
5659 sys.stdout.write(stdout)
5660 if opts.dry_run and len(stdout) > 0:
5661 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005662
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005663 # Similar code to above, but using yapf on .py files rather than clang-format
5664 # on C/C++ files
Aiden Bennerc08566e2018-10-03 17:52:42 +00005665 if opts.python and python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005666 yapf_tool = gclient_utils.FindExecutable('yapf')
5667 if yapf_tool is None:
5668 DieWithError('yapf not found in PATH')
5669
Aiden Bennerc08566e2018-10-03 17:52:42 +00005670 # If we couldn't find a yapf file we'll default to the chromium style
5671 # specified in depot_tools.
5672 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5673 chromium_default_yapf_style = os.path.join(depot_tools_path,
5674 YAPF_CONFIG_FILENAME)
5675
5676 # Note: yapf still seems to fix indentation of the entire file
5677 # even if line ranges are specified.
5678 # See https://github.com/google/yapf/issues/499
5679 if not opts.full:
5680 py_line_diffs = _ComputeDiffLineRanges(python_diff_files, upstream_commit)
5681
5682 # Used for caching.
5683 yapf_configs = {}
5684 for f in python_diff_files:
5685 # Find the yapf style config for the current file, defaults to depot
5686 # tools default.
5687 yapf_config = _FindYapfConfigFile(
5688 os.path.abspath(f), yapf_configs, top_dir,
5689 chromium_default_yapf_style)
5690
5691 cmd = [yapf_tool, '--style', yapf_config, f]
5692
5693 has_formattable_lines = False
5694 if not opts.full:
5695 # Only run yapf over changed line ranges.
5696 for diff_start, diff_len in py_line_diffs[f]:
5697 diff_end = diff_start + diff_len - 1
5698 # Yapf errors out if diff_end < diff_start but this
5699 # is a valid line range diff for a removal.
5700 if diff_end >= diff_start:
5701 has_formattable_lines = True
5702 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5703 # If all line diffs were removals we have nothing to format.
5704 if not has_formattable_lines:
5705 continue
5706
5707 if opts.diff or opts.dry_run:
5708 cmd += ['--diff']
5709 # Will return non-zero exit code if non-empty diff.
5710 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5711 if opts.diff:
5712 sys.stdout.write(stdout)
5713 elif len(stdout) > 0:
5714 return_value = 2
5715 else:
5716 cmd += ['-i']
5717 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005718
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005719 # Dart's formatter does not have the nice property of only operating on
5720 # modified chunks, so hard code full.
5721 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005722 try:
5723 command = [dart_format.FindDartFmtToolInChromiumTree()]
5724 if not opts.dry_run and not opts.diff:
5725 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005726 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005727
ppi@chromium.org6593d932016-03-03 15:41:15 +00005728 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005729 if opts.dry_run and stdout:
5730 return_value = 2
5731 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005732 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5733 'found in this checkout. Files in other languages are still '
5734 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005735
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005736 # Format GN build files. Always run on full build files for canonical form.
5737 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005738 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005739 if opts.dry_run or opts.diff:
5740 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005741 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005742 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5743 shell=sys.platform == 'win32',
5744 cwd=top_dir)
5745 if opts.dry_run and gn_ret == 2:
5746 return_value = 2 # Not formatted.
5747 elif opts.diff and gn_ret == 2:
5748 # TODO this should compute and print the actual diff.
5749 print("This change has GN build file diff for " + gn_diff_file)
5750 elif gn_ret != 0:
5751 # For non-dry run cases (and non-2 return values for dry-run), a
5752 # nonzero error code indicates a failure, probably because the file
5753 # doesn't parse.
5754 DieWithError("gn format failed on " + gn_diff_file +
5755 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005756
Ilya Shermane081cbe2017-08-15 17:51:04 -07005757 # Skip the metrics formatting from the global presubmit hook. These files have
5758 # a separate presubmit hook that issues an error if the files need formatting,
5759 # whereas the top-level presubmit script merely issues a warning. Formatting
5760 # these files is somewhat slow, so it's important not to duplicate the work.
5761 if not opts.presubmit:
5762 for xml_dir in GetDirtyMetricsDirs(diff_files):
5763 tool_dir = os.path.join(top_dir, xml_dir)
5764 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5765 if opts.dry_run or opts.diff:
5766 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005767 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005768 if opts.diff:
5769 sys.stdout.write(stdout)
5770 if opts.dry_run and stdout:
5771 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005772
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005773 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005774
Steven Holte2e664bf2017-04-21 13:10:47 -07005775def GetDirtyMetricsDirs(diff_files):
5776 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5777 metrics_xml_dirs = [
5778 os.path.join('tools', 'metrics', 'actions'),
5779 os.path.join('tools', 'metrics', 'histograms'),
5780 os.path.join('tools', 'metrics', 'rappor'),
5781 os.path.join('tools', 'metrics', 'ukm')]
5782 for xml_dir in metrics_xml_dirs:
5783 if any(file.startswith(xml_dir) for file in xml_diff_files):
5784 yield xml_dir
5785
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005786
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005787@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005788@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005789def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005790 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005791 _, args = parser.parse_args(args)
5792
5793 if len(args) != 1:
5794 parser.print_help()
5795 return 1
5796
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005797 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005798 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005799 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005800
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005801 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005802
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005803 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005804 output = RunGit(['config', '--local', '--get-regexp',
5805 r'branch\..*\.%s' % issueprefix],
5806 error_ok=True)
5807 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005808 if issue == target_issue:
5809 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005810
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005811 branches = []
5812 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005813 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005814 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005815 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005816 return 1
5817 if len(branches) == 1:
5818 RunGit(['checkout', branches[0]])
5819 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005820 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005821 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005822 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005823 which = raw_input('Choose by index: ')
5824 try:
5825 RunGit(['checkout', branches[int(which)]])
5826 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005827 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005828 return 1
5829
5830 return 0
5831
5832
maruel@chromium.org29404b52014-09-08 22:58:00 +00005833def CMDlol(parser, args):
5834 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005835 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005836 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5837 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5838 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005839 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005840 return 0
5841
5842
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005843class OptionParser(optparse.OptionParser):
5844 """Creates the option parse and add --verbose support."""
5845 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005846 optparse.OptionParser.__init__(
5847 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005848 self.add_option(
5849 '-v', '--verbose', action='count', default=0,
5850 help='Use 2 times for more debugging info')
5851
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005852 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005853 try:
5854 return self._parse_args(args)
5855 finally:
5856 # Regardless of success or failure of args parsing, we want to report
5857 # metrics, but only after logging has been initialized (if parsing
5858 # succeeded).
5859 global settings
5860 settings = Settings()
5861
5862 if not metrics.DISABLE_METRICS_COLLECTION:
5863 # GetViewVCUrl ultimately calls logging method.
5864 project_url = settings.GetViewVCUrl().strip('/+')
5865 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5866 metrics.collector.add('project_urls', [project_url])
5867
5868 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005869 # Create an optparse.Values object that will store only the actual passed
5870 # options, without the defaults.
5871 actual_options = optparse.Values()
5872 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5873 # Create an optparse.Values object with the default options.
5874 options = optparse.Values(self.get_default_values().__dict__)
5875 # Update it with the options passed by the user.
5876 options._update_careful(actual_options.__dict__)
5877 # Store the options passed by the user in an _actual_options attribute.
5878 # We store only the keys, and not the values, since the values can contain
5879 # arbitrary information, which might be PII.
5880 metrics.collector.add('arguments', actual_options.__dict__.keys())
5881
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005882 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005883 logging.basicConfig(
5884 level=levels[min(options.verbose, len(levels) - 1)],
5885 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5886 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005887
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005888 return options, args
5889
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005890
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005891def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005892 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005893 print('\nYour python version %s is unsupported, please upgrade.\n' %
5894 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005895 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005896
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005897 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005898 dispatcher = subcommand.CommandDispatcher(__name__)
5899 try:
5900 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005901 except auth.AuthenticationError as e:
5902 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005903 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005904 if e.code != 500:
5905 raise
5906 DieWithError(
5907 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5908 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005909 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005910
5911
5912if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005913 # These affect sys.stdout so do it outside of main() to simplify mocks in
5914 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005915 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005916 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005917 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005918 sys.exit(main(sys.argv[1:]))