blob: 4dd1cd3b5e329cdc91d90565bea944448a90e409 [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']
Dan Beamcf6df902018-11-08 01:48:37 +00002388 return data['revisions'][current_rev]['commit']['message'].encode(
2389 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002390
dsansomee2d6fd92016-09-08 00:10:47 -07002391 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002392 if gerrit_util.HasPendingChangeEdit(
2393 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002394 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002395 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002396 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002397 'unpublished edit. Either publish the edit in the Gerrit web UI '
2398 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002399
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002400 gerrit_util.DeletePendingChangeEdit(
2401 self._GetGerritHost(), self._GerritChangeIdentifier())
2402 gerrit_util.SetCommitMessage(
2403 self._GetGerritHost(), self._GerritChangeIdentifier(),
2404 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002405
Aaron Gable636b13f2017-07-14 10:42:48 -07002406 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002407 gerrit_util.SetReview(
2408 self._GetGerritHost(), self._GerritChangeIdentifier(),
2409 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002410
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002411 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002412 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002413 messages = self._GetChangeDetail(
2414 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2415 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002416 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002417
2418 # Build dictionary of file comments for easy access and sorting later.
2419 # {author+date: {path: {patchset: {line: url+message}}}}
2420 comments = collections.defaultdict(
2421 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2422 for path, line_comments in file_comments.iteritems():
2423 for comment in line_comments:
2424 if comment.get('tag', '').startswith('autogenerated'):
2425 continue
2426 key = (comment['author']['email'], comment['updated'])
2427 if comment.get('side', 'REVISION') == 'PARENT':
2428 patchset = 'Base'
2429 else:
2430 patchset = 'PS%d' % comment['patch_set']
2431 line = comment.get('line', 0)
2432 url = ('https://%s/c/%s/%s/%s#%s%s' %
2433 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2434 'b' if comment.get('side') == 'PARENT' else '',
2435 str(line) if line else ''))
2436 comments[key][path][patchset][line] = (url, comment['message'])
2437
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002438 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002439 for msg in messages:
2440 # Don't bother showing autogenerated messages.
2441 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2442 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002443 # Gerrit spits out nanoseconds.
2444 assert len(msg['date'].split('.')[-1]) == 9
2445 date = datetime.datetime.strptime(msg['date'][:-3],
2446 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002447 message = msg['message']
2448 key = (msg['author']['email'], msg['date'])
2449 if key in comments:
2450 message += '\n'
2451 for path, patchsets in sorted(comments.get(key, {}).items()):
2452 if readable:
2453 message += '\n%s' % path
2454 for patchset, lines in sorted(patchsets.items()):
2455 for line, (url, content) in sorted(lines.items()):
2456 if line:
2457 line_str = 'Line %d' % line
2458 path_str = '%s:%d:' % (path, line)
2459 else:
2460 line_str = 'File comment'
2461 path_str = '%s:0:' % path
2462 if readable:
2463 message += '\n %s, %s: %s' % (patchset, line_str, url)
2464 message += '\n %s\n' % content
2465 else:
2466 message += '\n%s ' % path_str
2467 message += '\n%s\n' % content
2468
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002469 summary.append(_CommentSummary(
2470 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002471 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002472 sender=msg['author']['email'],
2473 # These could be inferred from the text messages and correlated with
2474 # Code-Review label maximum, however this is not reliable.
2475 # Leaving as is until the need arises.
2476 approval=False,
2477 disapproval=False,
2478 ))
2479 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002480
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002481 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002482 gerrit_util.AbandonChange(
2483 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002484
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002485 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002486 gerrit_util.SubmitChange(
2487 self._GetGerritHost(), self._GerritChangeIdentifier(),
2488 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002489
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002490 def _GetChangeDetail(self, options=None, no_cache=False):
2491 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002492
2493 If fresh data is needed, set no_cache=True which will clear cache and
2494 thus new data will be fetched from Gerrit.
2495 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002496 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002497 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002498
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002499 # Optimization to avoid multiple RPCs:
2500 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2501 'CURRENT_COMMIT' not in options):
2502 options.append('CURRENT_COMMIT')
2503
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002504 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002505 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002506 options = [o.upper() for o in options]
2507
2508 # Check in cache first unless no_cache is True.
2509 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002510 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002511 else:
2512 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002513 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002514 # Assumption: data fetched before with extra options is suitable
2515 # for return for a smaller set of options.
2516 # For example, if we cached data for
2517 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2518 # and request is for options=[CURRENT_REVISION],
2519 # THEN we can return prior cached data.
2520 if options_set.issubset(cached_options_set):
2521 return data
2522
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002523 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002524 data = gerrit_util.GetChangeDetail(
2525 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002526 except gerrit_util.GerritError as e:
2527 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002528 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002529 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002530
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002531 self._detail_cache.setdefault(cache_key, []).append(
2532 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002533 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002534
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002535 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002536 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002537 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002538 data = gerrit_util.GetChangeCommit(
2539 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002540 except gerrit_util.GerritError as e:
2541 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002542 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002543 raise
agable32978d92016-11-01 12:55:02 -07002544 return data
2545
Olivier Robin75ee7252018-04-13 10:02:56 +02002546 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002547 if git_common.is_dirty_git_tree('land'):
2548 return 1
tandriid60367b2016-06-22 05:25:12 -07002549 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2550 if u'Commit-Queue' in detail.get('labels', {}):
2551 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002552 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2553 'which can test and land changes for you. '
2554 'Are you sure you wish to bypass it?\n',
2555 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002556
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002557 differs = True
tandriic4344b52016-08-29 06:04:54 -07002558 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002559 # Note: git diff outputs nothing if there is no diff.
2560 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002561 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002562 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002563 if detail['current_revision'] == last_upload:
2564 differs = False
2565 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002566 print('WARNING: Local branch contents differ from latest uploaded '
2567 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002568 if differs:
2569 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002570 confirm_or_exit(
2571 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2572 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002573 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002574 elif not bypass_hooks:
2575 hook_results = self.RunHook(
2576 committing=True,
2577 may_prompt=not force,
2578 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002579 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2580 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002581 if not hook_results.should_continue():
2582 return 1
2583
2584 self.SubmitIssue(wait_for_merge=True)
2585 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002586 links = self._GetChangeCommit().get('web_links', [])
2587 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002588 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002589 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002590 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002591 return 0
2592
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002593 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002594 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002595 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002596 assert not directory
2597 assert parsed_issue_arg.valid
2598
2599 self._changelist.issue = parsed_issue_arg.issue
2600
2601 if parsed_issue_arg.hostname:
2602 self._gerrit_host = parsed_issue_arg.hostname
2603 self._gerrit_server = 'https://%s' % self._gerrit_host
2604
tandriic2405f52016-10-10 08:13:15 -07002605 try:
2606 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002607 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002608 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002609
2610 if not parsed_issue_arg.patchset:
2611 # Use current revision by default.
2612 revision_info = detail['revisions'][detail['current_revision']]
2613 patchset = int(revision_info['_number'])
2614 else:
2615 patchset = parsed_issue_arg.patchset
2616 for revision_info in detail['revisions'].itervalues():
2617 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2618 break
2619 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002620 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002621 (parsed_issue_arg.patchset, self.GetIssue()))
2622
Aaron Gable697a91b2018-01-19 15:20:15 -08002623 remote_url = self._changelist.GetRemoteUrl()
2624 if remote_url.endswith('.git'):
2625 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002626 remote_url = remote_url.rstrip('/')
2627
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002628 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002629 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002630
2631 if remote_url != fetch_info['url']:
2632 DieWithError('Trying to patch a change from %s but this repo appears '
2633 'to be %s.' % (fetch_info['url'], remote_url))
2634
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002635 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002636
Aaron Gable62619a32017-06-16 08:22:09 -07002637 if force:
2638 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2639 print('Checked out commit for change %i patchset %i locally' %
2640 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002641 elif nocommit:
2642 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2643 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002644 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002645 RunGit(['cherry-pick', 'FETCH_HEAD'])
2646 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002647 (parsed_issue_arg.issue, patchset))
2648 print('Note: this created a local commit which does not have '
2649 'the same hash as the one uploaded for review. This will make '
2650 'uploading changes based on top of this branch difficult.\n'
2651 'If you want to do that, use "git cl patch --force" instead.')
2652
Stefan Zagerd08043c2017-10-12 12:07:02 -07002653 if self.GetBranch():
2654 self.SetIssue(parsed_issue_arg.issue)
2655 self.SetPatchset(patchset)
2656 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2657 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2658 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2659 else:
2660 print('WARNING: You are in detached HEAD state.\n'
2661 'The patch has been applied to your checkout, but you will not be '
2662 'able to upload a new patch set to the gerrit issue.\n'
2663 'Try using the \'-b\' option if you would like to work on a '
2664 'branch and/or upload a new patch set.')
2665
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002666 return 0
2667
2668 @staticmethod
2669 def ParseIssueURL(parsed_url):
2670 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2671 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002672 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2673 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002674 # Short urls like https://domain/<issue_number> can be used, but don't allow
2675 # specifying the patchset (you'd 404), but we allow that here.
2676 if parsed_url.path == '/':
2677 part = parsed_url.fragment
2678 else:
2679 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002680 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002681 if match:
2682 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002683 issue=int(match.group(3)),
2684 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002685 hostname=parsed_url.netloc,
2686 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002687 return None
2688
tandrii16e0b4e2016-06-07 10:34:28 -07002689 def _GerritCommitMsgHookCheck(self, offer_removal):
2690 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2691 if not os.path.exists(hook):
2692 return
2693 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2694 # custom developer made one.
2695 data = gclient_utils.FileRead(hook)
2696 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2697 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002698 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002699 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002700 'and may interfere with it in subtle ways.\n'
2701 'We recommend you remove the commit-msg hook.')
2702 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002703 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002704 gclient_utils.rm_file_or_tree(hook)
2705 print('Gerrit commit-msg hook removed.')
2706 else:
2707 print('OK, will keep Gerrit commit-msg hook in place.')
2708
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002709 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002710 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002711 if options.squash and options.no_squash:
2712 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002713
2714 if not options.squash and not options.no_squash:
2715 # Load default for user, repo, squash=true, in this order.
2716 options.squash = settings.GetSquashGerritUploads()
2717 elif options.no_squash:
2718 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002719
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002720 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002721 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002722
Aaron Gableb56ad332017-01-06 15:24:31 -08002723 # This may be None; default fallback value is determined in logic below.
2724 title = options.title
2725
Dominic Battre7d1c4842017-10-27 09:17:28 +02002726 # Extract bug number from branch name.
2727 bug = options.bug
2728 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2729 if not bug and match:
2730 bug = match.group(1)
2731
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002732 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002733 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002734 if self.GetIssue():
2735 # Try to get the message from a previous upload.
2736 message = self.GetDescription()
2737 if not message:
2738 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002739 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002740 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002741 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002742 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002743 # When uploading a subsequent patchset, -m|--message is taken
2744 # as the patchset title if --title was not provided.
2745 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002746 else:
2747 default_title = RunGit(
2748 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002749 if options.force:
2750 title = default_title
2751 else:
2752 title = ask_for_data(
2753 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002754 change_id = self._GetChangeDetail()['change_id']
2755 while True:
2756 footer_change_ids = git_footers.get_footer_change_id(message)
2757 if footer_change_ids == [change_id]:
2758 break
2759 if not footer_change_ids:
2760 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002761 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002762 continue
2763 # There is already a valid footer but with different or several ids.
2764 # Doing this automatically is non-trivial as we don't want to lose
2765 # existing other footers, yet we want to append just 1 desired
2766 # Change-Id. Thus, just create a new footer, but let user verify the
2767 # new description.
2768 message = '%s\n\nChange-Id: %s' % (message, change_id)
2769 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002770 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002771 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002772 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002773 'Please, check the proposed correction to the description, '
2774 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2775 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2776 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002777 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002778 if not options.force:
2779 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002780 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002781 message = change_desc.description
2782 if not message:
2783 DieWithError("Description is empty. Aborting...")
2784 # Continue the while loop.
2785 # Sanity check of this code - we should end up with proper message
2786 # footer.
2787 assert [change_id] == git_footers.get_footer_change_id(message)
2788 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002789 else: # if not self.GetIssue()
2790 if options.message:
2791 message = options.message
2792 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002793 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002794 if options.title:
2795 message = options.title + '\n\n' + message
2796 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002797
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002798 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002799 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002800 # On first upload, patchset title is always this string, while
2801 # --title flag gets converted to first line of message.
2802 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002803 if not change_desc.description:
2804 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002805 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002806 if len(change_ids) > 1:
2807 DieWithError('too many Change-Id footers, at most 1 allowed.')
2808 if not change_ids:
2809 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002810 change_desc.set_description(git_footers.add_footer_change_id(
2811 change_desc.description,
2812 GenerateGerritChangeId(change_desc.description)))
2813 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002814 assert len(change_ids) == 1
2815 change_id = change_ids[0]
2816
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002817 if options.reviewers or options.tbrs or options.add_owners_to:
2818 change_desc.update_reviewers(options.reviewers, options.tbrs,
2819 options.add_owners_to, change)
2820
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002821 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002822 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2823 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002824 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002825 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2826 desc_tempfile.write(change_desc.description)
2827 desc_tempfile.close()
2828 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2829 '-F', desc_tempfile.name]).strip()
2830 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002831 else:
2832 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002833 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002834 if not change_desc.description:
2835 DieWithError("Description is empty. Aborting...")
2836
2837 if not git_footers.get_footer_change_id(change_desc.description):
2838 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002839 change_desc.set_description(
2840 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002841 if options.reviewers or options.tbrs or options.add_owners_to:
2842 change_desc.update_reviewers(options.reviewers, options.tbrs,
2843 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002844 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002845 # For no-squash mode, we assume the remote called "origin" is the one we
2846 # want. It is not worthwhile to support different workflows for
2847 # no-squash mode.
2848 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002849 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2850
2851 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002852 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002853 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2854 ref_to_push)]).splitlines()
2855 if len(commits) > 1:
2856 print('WARNING: This will upload %d commits. Run the following command '
2857 'to see which commits will be uploaded: ' % len(commits))
2858 print('git log %s..%s' % (parent, ref_to_push))
2859 print('You can also use `git squash-branch` to squash these into a '
2860 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002861 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002862
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002863 if options.reviewers or options.tbrs or options.add_owners_to:
2864 change_desc.update_reviewers(options.reviewers, options.tbrs,
2865 options.add_owners_to, change)
2866
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002867 reviewers = sorted(change_desc.get_reviewers())
2868 # Add cc's from the CC_LIST and --cc flag (if any).
2869 if not options.private and not options.no_autocc:
2870 cc = self.GetCCList().split(',')
2871 else:
2872 cc = []
2873 if options.cc:
2874 cc.extend(options.cc)
2875 cc = filter(None, [email.strip() for email in cc])
2876 if change_desc.get_cced():
2877 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002878 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2879 valid_accounts = set(reviewers + cc)
2880 # TODO(crbug/877717): relax this for all hosts.
2881 else:
2882 valid_accounts = gerrit_util.ValidAccounts(
2883 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002884 logging.info('accounts %s are recognized, %s invalid',
2885 sorted(valid_accounts),
2886 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002887
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002888 # Extra options that can be specified at push time. Doc:
2889 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002890 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002891
Aaron Gable844cf292017-06-28 11:32:59 -07002892 # By default, new changes are started in WIP mode, and subsequent patchsets
2893 # don't send email. At any time, passing --send-mail will mark the change
2894 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002895 if options.send_mail:
2896 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002897 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002898 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002899 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002900 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002901 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002902
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002903 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002904 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002905
Aaron Gable9b713dd2016-12-14 16:04:21 -08002906 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002907 # Punctuation and whitespace in |title| must be percent-encoded.
2908 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002909
agablec6787972016-09-09 16:13:34 -07002910 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002911 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002912
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002913 for r in sorted(reviewers):
2914 if r in valid_accounts:
2915 refspec_opts.append('r=%s' % r)
2916 reviewers.remove(r)
2917 else:
2918 # TODO(tandrii): this should probably be a hard failure.
2919 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2920 % r)
2921 for c in sorted(cc):
2922 # refspec option will be rejected if cc doesn't correspond to an
2923 # account, even though REST call to add such arbitrary cc may succeed.
2924 if c in valid_accounts:
2925 refspec_opts.append('cc=%s' % c)
2926 cc.remove(c)
2927
rmistry9eadede2016-09-19 11:22:43 -07002928 if options.topic:
2929 # Documentation on Gerrit topics is here:
2930 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002931 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002932
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002933 if not change_desc.get_reviewers(tbr_only=True):
2934 # Change is not TBR, so we can inline setting other labels, too.
2935 # TODO(crbug.com/877717): make this working for TBR, too, by figuring out
2936 # max score for CR label somehow.
2937 if options.enable_auto_submit:
2938 refspec_opts.append('l=Auto-Submit+1')
2939 if options.use_commit_queue:
2940 refspec_opts.append('l=Commit-Queue+2')
2941 elif options.cq_dry_run:
2942 refspec_opts.append('l=Commit-Queue+1')
2943
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002944 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002945 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002946 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002947 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002948 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2949
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002950 refspec_suffix = ''
2951 if refspec_opts:
2952 refspec_suffix = '%' + ','.join(refspec_opts)
2953 assert ' ' not in refspec_suffix, (
2954 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2955 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2956
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002957 try:
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002958 before_push = time_time()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002959 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00002960 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemuredcefdc2018-11-08 14:41:42 +00002961 print_stdout=True,
Edward Lemur49c8eaf2018-11-07 22:13:12 +00002962 # Flush after every line: useful for seeing progress when running as
2963 # recipe.
2964 filter_fn=lambda _: sys.stdout.flush())
2965 push_returncode = 0
Edward Lemurfec80c42018-11-01 23:14:14 +00002966 except subprocess2.CalledProcessError as e:
2967 push_returncode = e.returncode
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002968 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002969 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002970 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002971 'credential problems:\n'
2972 ' git cl creds-check\n',
2973 change_desc)
Edward Lemurfec80c42018-11-01 23:14:14 +00002974 finally:
2975 metrics.collector.add_repeated('sub_commands', {
2976 'command': 'git push',
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002977 'execution_time': time_time() - before_push,
Edward Lemurfec80c42018-11-01 23:14:14 +00002978 'exit_code': push_returncode,
2979 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2980 })
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002981
2982 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002983 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002984 change_numbers = [m.group(1)
2985 for m in map(regex.match, push_stdout.splitlines())
2986 if m]
2987 if len(change_numbers) != 1:
2988 DieWithError(
2989 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002990 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002991 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002992 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002993
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002994 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002995 # GetIssue() is not set in case of non-squash uploads according to tests.
2996 # TODO(agable): non-squash uploads in git cl should be removed.
2997 gerrit_util.AddReviewers(
2998 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002999 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003000 reviewers, cc,
3001 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003002
Aaron Gablefd238082017-06-07 13:42:34 -07003003 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003004 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3005 score = 1
3006 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3007 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3008 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003009 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003010 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003011 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003012 msg='Self-approving for TBR',
3013 labels={'Code-Review': score})
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00003014 # Labels aren't set through refspec only if tbr is set (see check above).
3015 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
3016 options.cq_dry_run)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003017 return 0
3018
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003019 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3020 change_desc):
3021 """Computes parent of the generated commit to be uploaded to Gerrit.
3022
3023 Returns revision or a ref name.
3024 """
3025 if custom_cl_base:
3026 # Try to avoid creating additional unintended CLs when uploading, unless
3027 # user wants to take this risk.
3028 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3029 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3030 local_ref_of_target_remote])
3031 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003032 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003033 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3034 'If you proceed with upload, more than 1 CL may be created by '
3035 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3036 'If you are certain that specified base `%s` has already been '
3037 'uploaded to Gerrit as another CL, you may proceed.\n' %
3038 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3039 if not force:
3040 confirm_or_exit(
3041 'Do you take responsibility for cleaning up potential mess '
3042 'resulting from proceeding with upload?',
3043 action='upload')
3044 return custom_cl_base
3045
Aaron Gablef97e33d2017-03-30 15:44:27 -07003046 if remote != '.':
3047 return self.GetCommonAncestorWithUpstream()
3048
3049 # If our upstream branch is local, we base our squashed commit on its
3050 # squashed version.
3051 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3052
Aaron Gablef97e33d2017-03-30 15:44:27 -07003053 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003054 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003055
3056 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003057 # TODO(tandrii): consider checking parent change in Gerrit and using its
3058 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3059 # the tree hash of the parent branch. The upside is less likely bogus
3060 # requests to reupload parent change just because it's uploadhash is
3061 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003062 parent = RunGit(['config',
3063 'branch.%s.gerritsquashhash' % upstream_branch_name],
3064 error_ok=True).strip()
3065 # Verify that the upstream branch has been uploaded too, otherwise
3066 # Gerrit will create additional CLs when uploading.
3067 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3068 RunGitSilent(['rev-parse', parent + ':'])):
3069 DieWithError(
3070 '\nUpload upstream branch %s first.\n'
3071 'It is likely that this branch has been rebased since its last '
3072 'upload, so you just need to upload it again.\n'
3073 '(If you uploaded it with --no-squash, then branch dependencies '
3074 'are not supported, and you should reupload with --squash.)'
3075 % upstream_branch_name,
3076 change_desc)
3077 return parent
3078
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003079 def _AddChangeIdToCommitMessage(self, options, args):
3080 """Re-commits using the current message, assumes the commit hook is in
3081 place.
3082 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003083 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003084 git_command = ['commit', '--amend', '-m', log_desc]
3085 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003086 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003087 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003088 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003089 return new_log_desc
3090 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003091 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003092
Ravi Mistry31e7d562018-04-02 12:53:57 -04003093 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3094 """Sets labels on the change based on the provided flags."""
3095 labels = {}
3096 notify = None;
3097 if enable_auto_submit:
3098 labels['Auto-Submit'] = 1
3099 if use_commit_queue:
3100 labels['Commit-Queue'] = 2
3101 elif cq_dry_run:
3102 labels['Commit-Queue'] = 1
3103 notify = False
3104 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003105 gerrit_util.SetReview(
3106 self._GetGerritHost(),
3107 self._GerritChangeIdentifier(),
3108 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003109
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003110 def SetCQState(self, new_state):
3111 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003112 vote_map = {
3113 _CQState.NONE: 0,
3114 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003115 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003116 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003117 labels = {'Commit-Queue': vote_map[new_state]}
3118 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003119 gerrit_util.SetReview(
3120 self._GetGerritHost(), self._GerritChangeIdentifier(),
3121 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003122
tandriie113dfd2016-10-11 10:20:12 -07003123 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003124 try:
3125 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003126 except GerritChangeNotExists:
3127 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003128
3129 if data['status'] in ('ABANDONED', 'MERGED'):
3130 return 'CL %s is closed' % self.GetIssue()
3131
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003132 def GetTryJobProperties(self, patchset=None):
3133 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003134 data = self._GetChangeDetail(['ALL_REVISIONS'])
3135 patchset = int(patchset or self.GetPatchset())
3136 assert patchset
3137 revision_data = None # Pylint wants it to be defined.
3138 for revision_data in data['revisions'].itervalues():
3139 if int(revision_data['_number']) == patchset:
3140 break
3141 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003142 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003143 (patchset, self.GetIssue()))
3144 return {
3145 'patch_issue': self.GetIssue(),
3146 'patch_set': patchset or self.GetPatchset(),
3147 'patch_project': data['project'],
3148 'patch_storage': 'gerrit',
3149 'patch_ref': revision_data['fetch']['http']['ref'],
3150 'patch_repository_url': revision_data['fetch']['http']['url'],
3151 'patch_gerrit_url': self.GetCodereviewServer(),
3152 }
tandriie113dfd2016-10-11 10:20:12 -07003153
tandriide281ae2016-10-12 06:02:30 -07003154 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003155 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003156
Edward Lemur707d70b2018-02-07 00:50:14 +01003157 def GetReviewers(self):
3158 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00003159 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003160
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003161
3162_CODEREVIEW_IMPLEMENTATIONS = {
3163 'rietveld': _RietveldChangelistImpl,
3164 'gerrit': _GerritChangelistImpl,
3165}
3166
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003167
iannuccie53c9352016-08-17 14:40:40 -07003168def _add_codereview_issue_select_options(parser, extra=""):
3169 _add_codereview_select_options(parser)
3170
3171 text = ('Operate on this issue number instead of the current branch\'s '
3172 'implicit issue.')
3173 if extra:
3174 text += ' '+extra
3175 parser.add_option('-i', '--issue', type=int, help=text)
3176
3177
3178def _process_codereview_issue_select_options(parser, options):
3179 _process_codereview_select_options(parser, options)
3180 if options.issue is not None and not options.forced_codereview:
3181 parser.error('--issue must be specified with either --rietveld or --gerrit')
3182
3183
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003184def _add_codereview_select_options(parser):
3185 """Appends --gerrit and --rietveld options to force specific codereview."""
3186 parser.codereview_group = optparse.OptionGroup(
3187 parser, 'EXPERIMENTAL! Codereview override options')
3188 parser.add_option_group(parser.codereview_group)
3189 parser.codereview_group.add_option(
3190 '--gerrit', action='store_true',
3191 help='Force the use of Gerrit for codereview')
3192 parser.codereview_group.add_option(
3193 '--rietveld', action='store_true',
3194 help='Force the use of Rietveld for codereview')
3195
3196
3197def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003198 if options.rietveld:
3199 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003200 options.forced_codereview = None
3201 if options.gerrit:
3202 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003203
3204
tandriif9aefb72016-07-01 09:06:51 -07003205def _get_bug_line_values(default_project, bugs):
3206 """Given default_project and comma separated list of bugs, yields bug line
3207 values.
3208
3209 Each bug can be either:
3210 * a number, which is combined with default_project
3211 * string, which is left as is.
3212
3213 This function may produce more than one line, because bugdroid expects one
3214 project per line.
3215
3216 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3217 ['v8:123', 'chromium:789']
3218 """
3219 default_bugs = []
3220 others = []
3221 for bug in bugs.split(','):
3222 bug = bug.strip()
3223 if bug:
3224 try:
3225 default_bugs.append(int(bug))
3226 except ValueError:
3227 others.append(bug)
3228
3229 if default_bugs:
3230 default_bugs = ','.join(map(str, default_bugs))
3231 if default_project:
3232 yield '%s:%s' % (default_project, default_bugs)
3233 else:
3234 yield default_bugs
3235 for other in sorted(others):
3236 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3237 yield other
3238
3239
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003240class ChangeDescription(object):
3241 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003242 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003243 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003244 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003245 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003246 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3247 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3248 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3249 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003250
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003251 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003252 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003253
agable@chromium.org42c20792013-09-12 17:34:49 +00003254 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003255 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003256 return '\n'.join(self._description_lines)
3257
3258 def set_description(self, desc):
3259 if isinstance(desc, basestring):
3260 lines = desc.splitlines()
3261 else:
3262 lines = [line.rstrip() for line in desc]
3263 while lines and not lines[0]:
3264 lines.pop(0)
3265 while lines and not lines[-1]:
3266 lines.pop(-1)
3267 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003268
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003269 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3270 """Rewrites the R=/TBR= line(s) as a single line each.
3271
3272 Args:
3273 reviewers (list(str)) - list of additional emails to use for reviewers.
3274 tbrs (list(str)) - list of additional emails to use for TBRs.
3275 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3276 the change that are missing OWNER coverage. If this is not None, you
3277 must also pass a value for `change`.
3278 change (Change) - The Change that should be used for OWNERS lookups.
3279 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003280 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003281 assert isinstance(tbrs, list), tbrs
3282
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003283 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003284 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003285
3286 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003287 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003288
3289 reviewers = set(reviewers)
3290 tbrs = set(tbrs)
3291 LOOKUP = {
3292 'TBR': tbrs,
3293 'R': reviewers,
3294 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003295
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003296 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003297 regexp = re.compile(self.R_LINE)
3298 matches = [regexp.match(line) for line in self._description_lines]
3299 new_desc = [l for i, l in enumerate(self._description_lines)
3300 if not matches[i]]
3301 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003302
agable@chromium.org42c20792013-09-12 17:34:49 +00003303 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003304
3305 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003306 for match in matches:
3307 if not match:
3308 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003309 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3310
3311 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003312 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003313 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003314 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003315 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003316 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003317 LOOKUP[add_owners_to].update(
3318 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003319
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003320 # If any folks ended up in both groups, remove them from tbrs.
3321 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003322
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003323 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3324 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003325
3326 # Put the new lines in the description where the old first R= line was.
3327 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3328 if 0 <= line_loc < len(self._description_lines):
3329 if new_tbr_line:
3330 self._description_lines.insert(line_loc, new_tbr_line)
3331 if new_r_line:
3332 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003333 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003334 if new_r_line:
3335 self.append_footer(new_r_line)
3336 if new_tbr_line:
3337 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003338
Aaron Gable3a16ed12017-03-23 10:51:55 -07003339 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003340 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003341 self.set_description([
3342 '# Enter a description of the change.',
3343 '# This will be displayed on the codereview site.',
3344 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003345 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003346 '--------------------',
3347 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003348
agable@chromium.org42c20792013-09-12 17:34:49 +00003349 regexp = re.compile(self.BUG_LINE)
3350 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003351 prefix = settings.GetBugPrefix()
3352 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003353 if git_footer:
3354 self.append_footer('Bug: %s' % ', '.join(values))
3355 else:
3356 for value in values:
3357 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003358
agable@chromium.org42c20792013-09-12 17:34:49 +00003359 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003360 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003361 if not content:
3362 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003363 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003364
Bruce Dawson2377b012018-01-11 16:46:49 -08003365 # Strip off comments and default inserted "Bug:" line.
3366 clean_lines = [line.rstrip() for line in lines if not
3367 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003368 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003369 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003370 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003371
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003372 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003373 """Adds a footer line to the description.
3374
3375 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3376 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3377 that Gerrit footers are always at the end.
3378 """
3379 parsed_footer_line = git_footers.parse_footer(line)
3380 if parsed_footer_line:
3381 # Line is a gerrit footer in the form: Footer-Key: any value.
3382 # Thus, must be appended observing Gerrit footer rules.
3383 self.set_description(
3384 git_footers.add_footer(self.description,
3385 key=parsed_footer_line[0],
3386 value=parsed_footer_line[1]))
3387 return
3388
3389 if not self._description_lines:
3390 self._description_lines.append(line)
3391 return
3392
3393 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3394 if gerrit_footers:
3395 # git_footers.split_footers ensures that there is an empty line before
3396 # actual (gerrit) footers, if any. We have to keep it that way.
3397 assert top_lines and top_lines[-1] == ''
3398 top_lines, separator = top_lines[:-1], top_lines[-1:]
3399 else:
3400 separator = [] # No need for separator if there are no gerrit_footers.
3401
3402 prev_line = top_lines[-1] if top_lines else ''
3403 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3404 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3405 top_lines.append('')
3406 top_lines.append(line)
3407 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003408
tandrii99a72f22016-08-17 14:33:24 -07003409 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003410 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003411 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003412 reviewers = [match.group(2).strip()
3413 for match in matches
3414 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003415 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003416
bradnelsond975b302016-10-23 12:20:23 -07003417 def get_cced(self):
3418 """Retrieves the list of reviewers."""
3419 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3420 cced = [match.group(2).strip() for match in matches if match]
3421 return cleanup_list(cced)
3422
Nodir Turakulov23b82142017-11-16 11:04:25 -08003423 def get_hash_tags(self):
3424 """Extracts and sanitizes a list of Gerrit hashtags."""
3425 subject = (self._description_lines or ('',))[0]
3426 subject = re.sub(
3427 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3428
3429 tags = []
3430 start = 0
3431 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3432 while True:
3433 m = bracket_exp.match(subject, start)
3434 if not m:
3435 break
3436 tags.append(self.sanitize_hash_tag(m.group(1)))
3437 start = m.end()
3438
3439 if not tags:
3440 # Try "Tag: " prefix.
3441 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3442 if m:
3443 tags.append(self.sanitize_hash_tag(m.group(1)))
3444 return tags
3445
3446 @classmethod
3447 def sanitize_hash_tag(cls, tag):
3448 """Returns a sanitized Gerrit hash tag.
3449
3450 A sanitized hashtag can be used as a git push refspec parameter value.
3451 """
3452 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3453
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003454 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3455 """Updates this commit description given the parent.
3456
3457 This is essentially what Gnumbd used to do.
3458 Consult https://goo.gl/WMmpDe for more details.
3459 """
3460 assert parent_msg # No, orphan branch creation isn't supported.
3461 assert parent_hash
3462 assert dest_ref
3463 parent_footer_map = git_footers.parse_footers(parent_msg)
3464 # This will also happily parse svn-position, which GnumbD is no longer
3465 # supporting. While we'd generate correct footers, the verifier plugin
3466 # installed in Gerrit will block such commit (ie git push below will fail).
3467 parent_position = git_footers.get_position(parent_footer_map)
3468
3469 # Cherry-picks may have last line obscuring their prior footers,
3470 # from git_footers perspective. This is also what Gnumbd did.
3471 cp_line = None
3472 if (self._description_lines and
3473 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3474 cp_line = self._description_lines.pop()
3475
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003476 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003477
3478 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3479 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003480 for i, line in enumerate(footer_lines):
3481 k, v = git_footers.parse_footer(line) or (None, None)
3482 if k and k.startswith('Cr-'):
3483 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003484
3485 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003486 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003487 if parent_position[0] == dest_ref:
3488 # Same branch as parent.
3489 number = int(parent_position[1]) + 1
3490 else:
3491 number = 1 # New branch, and extra lineage.
3492 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3493 int(parent_position[1])))
3494
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003495 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3496 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003497
3498 self._description_lines = top_lines
3499 if cp_line:
3500 self._description_lines.append(cp_line)
3501 if self._description_lines[-1] != '':
3502 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003503 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003504
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003505
Aaron Gablea1bab272017-04-11 16:38:18 -07003506def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003507 """Retrieves the reviewers that approved a CL from the issue properties with
3508 messages.
3509
3510 Note that the list may contain reviewers that are not committer, thus are not
3511 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003512
3513 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003514 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003515 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003516 return sorted(
3517 set(
3518 message['sender']
3519 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003520 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003521 )
3522 )
3523
3524
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003525def FindCodereviewSettingsFile(filename='codereview.settings'):
3526 """Finds the given file starting in the cwd and going up.
3527
3528 Only looks up to the top of the repository unless an
3529 'inherit-review-settings-ok' file exists in the root of the repository.
3530 """
3531 inherit_ok_file = 'inherit-review-settings-ok'
3532 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003533 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003534 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3535 root = '/'
3536 while True:
3537 if filename in os.listdir(cwd):
3538 if os.path.isfile(os.path.join(cwd, filename)):
3539 return open(os.path.join(cwd, filename))
3540 if cwd == root:
3541 break
3542 cwd = os.path.dirname(cwd)
3543
3544
3545def LoadCodereviewSettingsFromFile(fileobj):
3546 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003547 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003548
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003549 def SetProperty(name, setting, unset_error_ok=False):
3550 fullname = 'rietveld.' + name
3551 if setting in keyvals:
3552 RunGit(['config', fullname, keyvals[setting]])
3553 else:
3554 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3555
tandrii48df5812016-10-17 03:55:37 -07003556 if not keyvals.get('GERRIT_HOST', False):
3557 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003558 # Only server setting is required. Other settings can be absent.
3559 # In that case, we ignore errors raised during option deletion attempt.
3560 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003561 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003562 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3563 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003564 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003565 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3566 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003567 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003568 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3569 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003570
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003571 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003572 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003573
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003574 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003575 RunGit(['config', 'gerrit.squash-uploads',
3576 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003577
tandrii@chromium.org28253532016-04-14 13:46:56 +00003578 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003579 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003580 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3581
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003582 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003583 # should be of the form
3584 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3585 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003586 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3587 keyvals['ORIGIN_URL_CONFIG']])
3588
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003589
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003590def urlretrieve(source, destination):
3591 """urllib is broken for SSL connections via a proxy therefore we
3592 can't use urllib.urlretrieve()."""
3593 with open(destination, 'w') as f:
3594 f.write(urllib2.urlopen(source).read())
3595
3596
ukai@chromium.org712d6102013-11-27 00:52:58 +00003597def hasSheBang(fname):
3598 """Checks fname is a #! script."""
3599 with open(fname) as f:
3600 return f.read(2).startswith('#!')
3601
3602
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003603# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3604def DownloadHooks(*args, **kwargs):
3605 pass
3606
3607
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003608def DownloadGerritHook(force):
3609 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003610
3611 Args:
3612 force: True to update hooks. False to install hooks if not present.
3613 """
3614 if not settings.GetIsGerrit():
3615 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003616 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003617 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3618 if not os.access(dst, os.X_OK):
3619 if os.path.exists(dst):
3620 if not force:
3621 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003622 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003623 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003624 if not hasSheBang(dst):
3625 DieWithError('Not a script: %s\n'
3626 'You need to download from\n%s\n'
3627 'into .git/hooks/commit-msg and '
3628 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003629 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3630 except Exception:
3631 if os.path.exists(dst):
3632 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003633 DieWithError('\nFailed to download hooks.\n'
3634 'You need to download from\n%s\n'
3635 'into .git/hooks/commit-msg and '
3636 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003637
3638
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003639def GetRietveldCodereviewSettingsInteractively():
3640 """Prompt the user for settings."""
3641 server = settings.GetDefaultServerUrl(error_ok=True)
3642 prompt = 'Rietveld server (host[:port])'
3643 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3644 newserver = ask_for_data(prompt + ':')
3645 if not server and not newserver:
3646 newserver = DEFAULT_SERVER
3647 if newserver:
3648 newserver = gclient_utils.UpgradeToHttps(newserver)
3649 if newserver != server:
3650 RunGit(['config', 'rietveld.server', newserver])
3651
3652 def SetProperty(initial, caption, name, is_url):
3653 prompt = caption
3654 if initial:
3655 prompt += ' ("x" to clear) [%s]' % initial
3656 new_val = ask_for_data(prompt + ':')
3657 if new_val == 'x':
3658 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3659 elif new_val:
3660 if is_url:
3661 new_val = gclient_utils.UpgradeToHttps(new_val)
3662 if new_val != initial:
3663 RunGit(['config', 'rietveld.' + name, new_val])
3664
3665 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3666 SetProperty(settings.GetDefaultPrivateFlag(),
3667 'Private flag (rietveld only)', 'private', False)
3668 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3669 'tree-status-url', False)
3670 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3671 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3672 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3673 'run-post-upload-hook', False)
3674
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003675
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003676class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003677 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003678
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003679 _GOOGLESOURCE = 'googlesource.com'
3680
3681 def __init__(self):
3682 # Cached list of [host, identity, source], where source is either
3683 # .gitcookies or .netrc.
3684 self._all_hosts = None
3685
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003686 def ensure_configured_gitcookies(self):
3687 """Runs checks and suggests fixes to make git use .gitcookies from default
3688 path."""
3689 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3690 configured_path = RunGitSilent(
3691 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003692 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003693 if configured_path:
3694 self._ensure_default_gitcookies_path(configured_path, default)
3695 else:
3696 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003697
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003698 @staticmethod
3699 def _ensure_default_gitcookies_path(configured_path, default_path):
3700 assert configured_path
3701 if configured_path == default_path:
3702 print('git is already configured to use your .gitcookies from %s' %
3703 configured_path)
3704 return
3705
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003706 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003707 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3708 (configured_path, default_path))
3709
3710 if not os.path.exists(configured_path):
3711 print('However, your configured .gitcookies file is missing.')
3712 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3713 action='reconfigure')
3714 RunGit(['config', '--global', 'http.cookiefile', default_path])
3715 return
3716
3717 if os.path.exists(default_path):
3718 print('WARNING: default .gitcookies file already exists %s' %
3719 default_path)
3720 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3721 default_path)
3722
3723 confirm_or_exit('Move existing .gitcookies to default location?',
3724 action='move')
3725 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003726 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003727 print('Moved and reconfigured git to use .gitcookies from %s' %
3728 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003729
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003730 @staticmethod
3731 def _configure_gitcookies_path(default_path):
3732 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3733 if os.path.exists(netrc_path):
3734 print('You seem to be using outdated .netrc for git credentials: %s' %
3735 netrc_path)
3736 print('This tool will guide you through setting up recommended '
3737 '.gitcookies store for git credentials.\n'
3738 '\n'
3739 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3740 ' git config --global --unset http.cookiefile\n'
3741 ' mv %s %s.backup\n\n' % (default_path, default_path))
3742 confirm_or_exit(action='setup .gitcookies')
3743 RunGit(['config', '--global', 'http.cookiefile', default_path])
3744 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003745
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003746 def get_hosts_with_creds(self, include_netrc=False):
3747 if self._all_hosts is None:
3748 a = gerrit_util.CookiesAuthenticator()
3749 self._all_hosts = [
3750 (h, u, s)
3751 for h, u, s in itertools.chain(
3752 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3753 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3754 )
3755 if h.endswith(self._GOOGLESOURCE)
3756 ]
3757
3758 if include_netrc:
3759 return self._all_hosts
3760 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3761
3762 def print_current_creds(self, include_netrc=False):
3763 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3764 if not hosts:
3765 print('No Git/Gerrit credentials found')
3766 return
3767 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3768 header = [('Host', 'User', 'Which file'),
3769 ['=' * l for l in lengths]]
3770 for row in (header + hosts):
3771 print('\t'.join((('%%+%ds' % l) % s)
3772 for l, s in zip(lengths, row)))
3773
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003774 @staticmethod
3775 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003776 """Parses identity "git-<username>.domain" into <username> and domain."""
3777 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003778 # distinguishable from sub-domains. But we do know typical domains:
3779 if identity.endswith('.chromium.org'):
3780 domain = 'chromium.org'
3781 username = identity[:-len('.chromium.org')]
3782 else:
3783 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003784 if username.startswith('git-'):
3785 username = username[len('git-'):]
3786 return username, domain
3787
3788 def _get_usernames_of_domain(self, domain):
3789 """Returns list of usernames referenced by .gitcookies in a given domain."""
3790 identities_by_domain = {}
3791 for _, identity, _ in self.get_hosts_with_creds():
3792 username, domain = self._parse_identity(identity)
3793 identities_by_domain.setdefault(domain, []).append(username)
3794 return identities_by_domain.get(domain)
3795
3796 def _canonical_git_googlesource_host(self, host):
3797 """Normalizes Gerrit hosts (with '-review') to Git host."""
3798 assert host.endswith(self._GOOGLESOURCE)
3799 # Prefix doesn't include '.' at the end.
3800 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3801 if prefix.endswith('-review'):
3802 prefix = prefix[:-len('-review')]
3803 return prefix + '.' + self._GOOGLESOURCE
3804
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003805 def _canonical_gerrit_googlesource_host(self, host):
3806 git_host = self._canonical_git_googlesource_host(host)
3807 prefix = git_host.split('.', 1)[0]
3808 return prefix + '-review.' + self._GOOGLESOURCE
3809
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003810 def _get_counterpart_host(self, host):
3811 assert host.endswith(self._GOOGLESOURCE)
3812 git = self._canonical_git_googlesource_host(host)
3813 gerrit = self._canonical_gerrit_googlesource_host(git)
3814 return git if gerrit == host else gerrit
3815
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003816 def has_generic_host(self):
3817 """Returns whether generic .googlesource.com has been configured.
3818
3819 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3820 """
3821 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3822 if host == '.' + self._GOOGLESOURCE:
3823 return True
3824 return False
3825
3826 def _get_git_gerrit_identity_pairs(self):
3827 """Returns map from canonic host to pair of identities (Git, Gerrit).
3828
3829 One of identities might be None, meaning not configured.
3830 """
3831 host_to_identity_pairs = {}
3832 for host, identity, _ in self.get_hosts_with_creds():
3833 canonical = self._canonical_git_googlesource_host(host)
3834 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3835 idx = 0 if canonical == host else 1
3836 pair[idx] = identity
3837 return host_to_identity_pairs
3838
3839 def get_partially_configured_hosts(self):
3840 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003841 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3842 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3843 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003844
3845 def get_conflicting_hosts(self):
3846 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003847 host
3848 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003849 if None not in (i1, i2) and i1 != i2)
3850
3851 def get_duplicated_hosts(self):
3852 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3853 return set(host for host, count in counters.iteritems() if count > 1)
3854
3855 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3856 'chromium.googlesource.com': 'chromium.org',
3857 'chrome-internal.googlesource.com': 'google.com',
3858 }
3859
3860 def get_hosts_with_wrong_identities(self):
3861 """Finds hosts which **likely** reference wrong identities.
3862
3863 Note: skips hosts which have conflicting identities for Git and Gerrit.
3864 """
3865 hosts = set()
3866 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3867 pair = self._get_git_gerrit_identity_pairs().get(host)
3868 if pair and pair[0] == pair[1]:
3869 _, domain = self._parse_identity(pair[0])
3870 if domain != expected:
3871 hosts.add(host)
3872 return hosts
3873
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003874 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003875 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003876 hosts = sorted(hosts)
3877 assert hosts
3878 if extra_column_func is None:
3879 extras = [''] * len(hosts)
3880 else:
3881 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003882 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3883 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003884 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003885 lines.append(tmpl % he)
3886 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003887
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003888 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003889 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003890 yield ('.googlesource.com wildcard record detected',
3891 ['Chrome Infrastructure team recommends to list full host names '
3892 'explicitly.'],
3893 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003894
3895 dups = self.get_duplicated_hosts()
3896 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003897 yield ('The following hosts were defined twice',
3898 self._format_hosts(dups),
3899 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003900
3901 partial = self.get_partially_configured_hosts()
3902 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003903 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3904 'These hosts are missing',
3905 self._format_hosts(partial, lambda host: 'but %s defined' %
3906 self._get_counterpart_host(host)),
3907 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003908
3909 conflicting = self.get_conflicting_hosts()
3910 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003911 yield ('The following Git hosts have differing credentials from their '
3912 'Gerrit counterparts',
3913 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3914 tuple(self._get_git_gerrit_identity_pairs()[host])),
3915 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003916
3917 wrong = self.get_hosts_with_wrong_identities()
3918 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003919 yield ('These hosts likely use wrong identity',
3920 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3921 (self._get_git_gerrit_identity_pairs()[host][0],
3922 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3923 wrong)
3924
3925 def find_and_report_problems(self):
3926 """Returns True if there was at least one problem, else False."""
3927 found = False
3928 bad_hosts = set()
3929 for title, sublines, hosts in self._find_problems():
3930 if not found:
3931 found = True
3932 print('\n\n.gitcookies problem report:\n')
3933 bad_hosts.update(hosts or [])
3934 print(' %s%s' % (title , (':' if sublines else '')))
3935 if sublines:
3936 print()
3937 print(' %s' % '\n '.join(sublines))
3938 print()
3939
3940 if bad_hosts:
3941 assert found
3942 print(' You can manually remove corresponding lines in your %s file and '
3943 'visit the following URLs with correct account to generate '
3944 'correct credential lines:\n' %
3945 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3946 print(' %s' % '\n '.join(sorted(set(
3947 gerrit_util.CookiesAuthenticator().get_new_password_url(
3948 self._canonical_git_googlesource_host(host))
3949 for host in bad_hosts
3950 ))))
3951 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003952
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003953
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003954@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003955def CMDcreds_check(parser, args):
3956 """Checks credentials and suggests changes."""
3957 _, _ = parser.parse_args(args)
3958
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003959 # Code below checks .gitcookies. Abort if using something else.
3960 authn = gerrit_util.Authenticator.get()
3961 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3962 if isinstance(authn, gerrit_util.GceAuthenticator):
3963 DieWithError(
3964 'This command is not designed for GCE, are you on a bot?\n'
3965 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3966 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003967 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003968 'This command is not designed for bot environment. It checks '
3969 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003970
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003971 checker = _GitCookiesChecker()
3972 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003973
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003974 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003975 checker.print_current_creds(include_netrc=True)
3976
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003977 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003978 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003979 return 0
3980 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003981
3982
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003983@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003984@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003985def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003986 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003987
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003988 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07003989 # TODO(tandrii): remove this once we switch to Gerrit.
3990 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003991 parser.add_option('--activate-update', action='store_true',
3992 help='activate auto-updating [rietveld] section in '
3993 '.git/config')
3994 parser.add_option('--deactivate-update', action='store_true',
3995 help='deactivate auto-updating [rietveld] section in '
3996 '.git/config')
3997 options, args = parser.parse_args(args)
3998
3999 if options.deactivate_update:
4000 RunGit(['config', 'rietveld.autoupdate', 'false'])
4001 return
4002
4003 if options.activate_update:
4004 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4005 return
4006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004007 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004008 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004009 return 0
4010
4011 url = args[0]
4012 if not url.endswith('codereview.settings'):
4013 url = os.path.join(url, 'codereview.settings')
4014
4015 # Load code review settings and download hooks (if available).
4016 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4017 return 0
4018
4019
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004020@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004021def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004022 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004023 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4024 branch = ShortBranchName(branchref)
4025 _, args = parser.parse_args(args)
4026 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004027 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004028 return RunGit(['config', 'branch.%s.base-url' % branch],
4029 error_ok=False).strip()
4030 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004031 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004032 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4033 error_ok=False).strip()
4034
4035
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004036def color_for_status(status):
4037 """Maps a Changelist status to color, for CMDstatus and other tools."""
4038 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004039 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004040 'waiting': Fore.BLUE,
4041 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004042 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004043 'lgtm': Fore.GREEN,
4044 'commit': Fore.MAGENTA,
4045 'closed': Fore.CYAN,
4046 'error': Fore.WHITE,
4047 }.get(status, Fore.WHITE)
4048
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004049
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004050def get_cl_statuses(changes, fine_grained, max_processes=None):
4051 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004052
4053 If fine_grained is true, this will fetch CL statuses from the server.
4054 Otherwise, simply indicate if there's a matching url for the given branches.
4055
4056 If max_processes is specified, it is used as the maximum number of processes
4057 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4058 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004059
4060 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004061 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004062 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004063 upload.verbosity = 0
4064
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004065 if not changes:
4066 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004067
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004068 if not fine_grained:
4069 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004070 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004071 for cl in changes:
4072 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004073 return
4074
4075 # First, sort out authentication issues.
4076 logging.debug('ensuring credentials exist')
4077 for cl in changes:
4078 cl.EnsureAuthenticated(force=False, refresh=True)
4079
4080 def fetch(cl):
4081 try:
4082 return (cl, cl.GetStatus())
4083 except:
4084 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004085 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004086 raise
4087
4088 threads_count = len(changes)
4089 if max_processes:
4090 threads_count = max(1, min(threads_count, max_processes))
4091 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4092
4093 pool = ThreadPool(threads_count)
4094 fetched_cls = set()
4095 try:
4096 it = pool.imap_unordered(fetch, changes).__iter__()
4097 while True:
4098 try:
4099 cl, status = it.next(timeout=5)
4100 except multiprocessing.TimeoutError:
4101 break
4102 fetched_cls.add(cl)
4103 yield cl, status
4104 finally:
4105 pool.close()
4106
4107 # Add any branches that failed to fetch.
4108 for cl in set(changes) - fetched_cls:
4109 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004110
rmistry@google.com2dd99862015-06-22 12:22:18 +00004111
4112def upload_branch_deps(cl, args):
4113 """Uploads CLs of local branches that are dependents of the current branch.
4114
4115 If the local branch dependency tree looks like:
4116 test1 -> test2.1 -> test3.1
4117 -> test3.2
4118 -> test2.2 -> test3.3
4119
4120 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4121 run on the dependent branches in this order:
4122 test2.1, test3.1, test3.2, test2.2, test3.3
4123
4124 Note: This function does not rebase your local dependent branches. Use it when
4125 you make a change to the parent branch that will not conflict with its
4126 dependent branches, and you would like their dependencies updated in
4127 Rietveld.
4128 """
4129 if git_common.is_dirty_git_tree('upload-branch-deps'):
4130 return 1
4131
4132 root_branch = cl.GetBranch()
4133 if root_branch is None:
4134 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4135 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004136 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00004137 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4138 'patchset dependencies without an uploaded CL.')
4139
4140 branches = RunGit(['for-each-ref',
4141 '--format=%(refname:short) %(upstream:short)',
4142 'refs/heads'])
4143 if not branches:
4144 print('No local branches found.')
4145 return 0
4146
4147 # Create a dictionary of all local branches to the branches that are dependent
4148 # on it.
4149 tracked_to_dependents = collections.defaultdict(list)
4150 for b in branches.splitlines():
4151 tokens = b.split()
4152 if len(tokens) == 2:
4153 branch_name, tracked = tokens
4154 tracked_to_dependents[tracked].append(branch_name)
4155
vapiera7fbd5a2016-06-16 09:17:49 -07004156 print()
4157 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004158 dependents = []
4159 def traverse_dependents_preorder(branch, padding=''):
4160 dependents_to_process = tracked_to_dependents.get(branch, [])
4161 padding += ' '
4162 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004163 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004164 dependents.append(dependent)
4165 traverse_dependents_preorder(dependent, padding)
4166 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004167 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004168
4169 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004170 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004171 return 0
4172
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004173 confirm_or_exit('This command will checkout all dependent branches and run '
4174 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004175
rmistry@google.com2dd99862015-06-22 12:22:18 +00004176 # Record all dependents that failed to upload.
4177 failures = {}
4178 # Go through all dependents, checkout the branch and upload.
4179 try:
4180 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004181 print()
4182 print('--------------------------------------')
4183 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004184 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004185 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004186 try:
4187 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004188 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004189 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004190 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004191 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004192 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004193 finally:
4194 # Swap back to the original root branch.
4195 RunGit(['checkout', '-q', root_branch])
4196
vapiera7fbd5a2016-06-16 09:17:49 -07004197 print()
4198 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004199 for dependent_branch in dependents:
4200 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004201 print(' %s : %s' % (dependent_branch, upload_status))
4202 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004203
4204 return 0
4205
4206
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004207@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004208def CMDarchive(parser, args):
4209 """Archives and deletes branches associated with closed changelists."""
4210 parser.add_option(
4211 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004212 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004213 parser.add_option(
4214 '-f', '--force', action='store_true',
4215 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004216 parser.add_option(
4217 '-d', '--dry-run', action='store_true',
4218 help='Skip the branch tagging and removal steps.')
4219 parser.add_option(
4220 '-t', '--notags', action='store_true',
4221 help='Do not tag archived branches. '
4222 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004223
4224 auth.add_auth_options(parser)
4225 options, args = parser.parse_args(args)
4226 if args:
4227 parser.error('Unsupported args: %s' % ' '.join(args))
4228 auth_config = auth.extract_auth_config_from_options(options)
4229
4230 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4231 if not branches:
4232 return 0
4233
vapiera7fbd5a2016-06-16 09:17:49 -07004234 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004235 changes = [Changelist(branchref=b, auth_config=auth_config)
4236 for b in branches.splitlines()]
4237 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4238 statuses = get_cl_statuses(changes,
4239 fine_grained=True,
4240 max_processes=options.maxjobs)
4241 proposal = [(cl.GetBranch(),
4242 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4243 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00004244 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07004245 proposal.sort()
4246
4247 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004248 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004249 return 0
4250
4251 current_branch = GetCurrentBranch()
4252
vapiera7fbd5a2016-06-16 09:17:49 -07004253 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004254 if options.notags:
4255 for next_item in proposal:
4256 print(' ' + next_item[0])
4257 else:
4258 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4259 for next_item in proposal:
4260 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004261
kmarshall9249e012016-08-23 12:02:16 -07004262 # Quit now on precondition failure or if instructed by the user, either
4263 # via an interactive prompt or by command line flags.
4264 if options.dry_run:
4265 print('\nNo changes were made (dry run).\n')
4266 return 0
4267 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004268 print('You are currently on a branch \'%s\' which is associated with a '
4269 'closed codereview issue, so archive cannot proceed. Please '
4270 'checkout another branch and run this command again.' %
4271 current_branch)
4272 return 1
kmarshall9249e012016-08-23 12:02:16 -07004273 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004274 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4275 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004276 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004277 return 1
4278
4279 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004280 if not options.notags:
4281 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004282 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004283
vapiera7fbd5a2016-06-16 09:17:49 -07004284 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004285
4286 return 0
4287
4288
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004289@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004290def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004291 """Show status of changelists.
4292
4293 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004294 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004295 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004296 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004297 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004298 - Magenta in the commit queue
4299 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004300 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004301
4302 Also see 'git cl comments'.
4303 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004304 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004305 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004306 parser.add_option('-f', '--fast', action='store_true',
4307 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004308 parser.add_option(
4309 '-j', '--maxjobs', action='store', type=int,
4310 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004311
4312 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004313 _add_codereview_issue_select_options(
4314 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004315 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004316 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004317 if args:
4318 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004319 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004320
iannuccie53c9352016-08-17 14:40:40 -07004321 if options.issue is not None and not options.field:
4322 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004323
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004324 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004325 cl = Changelist(auth_config=auth_config, issue=options.issue,
4326 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004327 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004328 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004329 elif options.field == 'id':
4330 issueid = cl.GetIssue()
4331 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004332 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004333 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004334 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004335 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004336 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004337 elif options.field == 'status':
4338 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004339 elif options.field == 'url':
4340 url = cl.GetIssueURL()
4341 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004342 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004343 return 0
4344
4345 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4346 if not branches:
4347 print('No local branch found.')
4348 return 0
4349
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004350 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004351 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004352 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004353 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004354 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004355 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004356 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004357
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004358 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004359 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4360 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4361 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004362 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004363 c, status = output.next()
4364 branch_statuses[c.GetBranch()] = status
4365 status = branch_statuses.pop(branch)
4366 url = cl.GetIssueURL()
4367 if url and (not status or status == 'error'):
4368 # The issue probably doesn't exist anymore.
4369 url += ' (broken)'
4370
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004371 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004372 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004373 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004374 color = ''
4375 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004376 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004377 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004378 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004379 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004380
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004381
4382 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004383 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004384 print('Current branch: %s' % branch)
4385 for cl in changes:
4386 if cl.GetBranch() == branch:
4387 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004388 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004389 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004390 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004391 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004392 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004393 print('Issue description:')
4394 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004395 return 0
4396
4397
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004398def colorize_CMDstatus_doc():
4399 """To be called once in main() to add colors to git cl status help."""
4400 colors = [i for i in dir(Fore) if i[0].isupper()]
4401
4402 def colorize_line(line):
4403 for color in colors:
4404 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004405 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004406 indent = len(line) - len(line.lstrip(' ')) + 1
4407 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4408 return line
4409
4410 lines = CMDstatus.__doc__.splitlines()
4411 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4412
4413
phajdan.jre328cf92016-08-22 04:12:17 -07004414def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004415 if path == '-':
4416 json.dump(contents, sys.stdout)
4417 else:
4418 with open(path, 'w') as f:
4419 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004420
4421
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004422@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004423@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004424def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004425 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004426
4427 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004428 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004429 parser.add_option('-r', '--reverse', action='store_true',
4430 help='Lookup the branch(es) for the specified issues. If '
4431 'no issues are specified, all branches with mapped '
4432 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004433 parser.add_option('--json',
4434 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004435 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004436 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004437 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004438
dnj@chromium.org406c4402015-03-03 17:22:28 +00004439 if options.reverse:
4440 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004441 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004442 # Reverse issue lookup.
4443 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004444
4445 git_config = {}
4446 for config in RunGit(['config', '--get-regexp',
4447 r'branch\..*issue']).splitlines():
4448 name, _space, val = config.partition(' ')
4449 git_config[name] = val
4450
dnj@chromium.org406c4402015-03-03 17:22:28 +00004451 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004452 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4453 config_key = _git_branch_config_key(ShortBranchName(branch),
4454 cls.IssueConfigKey())
4455 issue = git_config.get(config_key)
4456 if issue:
4457 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004458 if not args:
4459 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004460 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004461 for issue in args:
4462 if not issue:
4463 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004464 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004465 print('Branch for issue number %s: %s' % (
4466 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004467 if options.json:
4468 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004469 return 0
4470
4471 if len(args) > 0:
4472 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4473 if not issue.valid:
4474 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4475 'or no argument to list it.\n'
4476 'Maybe you want to run git cl status?')
4477 cl = Changelist(codereview=issue.codereview)
4478 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004479 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004480 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004481 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4482 if options.json:
4483 write_json(options.json, {
4484 'issue': cl.GetIssue(),
4485 'issue_url': cl.GetIssueURL(),
4486 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004487 return 0
4488
4489
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004490@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004491def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004492 """Shows or posts review comments for any changelist."""
4493 parser.add_option('-a', '--add-comment', dest='comment',
4494 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004495 parser.add_option('-p', '--publish', action='store_true',
4496 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004497 parser.add_option('-i', '--issue', dest='issue',
4498 help='review issue id (defaults to current issue). '
4499 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004500 parser.add_option('-m', '--machine-readable', dest='readable',
4501 action='store_false', default=True,
4502 help='output comments in a format compatible with '
4503 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004504 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004505 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004506 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004507 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004508 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004509 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004510 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004511
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004512 issue = None
4513 if options.issue:
4514 try:
4515 issue = int(options.issue)
4516 except ValueError:
4517 DieWithError('A review issue id is expected to be a number')
4518
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004519 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4520
4521 if not cl.IsGerrit():
4522 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004523
4524 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004525 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004526 return 0
4527
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004528 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4529 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004530 for comment in summary:
4531 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004532 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004533 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004534 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004535 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004536 color = Fore.MAGENTA
4537 else:
4538 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004539 print('\n%s%s %s%s\n%s' % (
4540 color,
4541 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4542 comment.sender,
4543 Fore.RESET,
4544 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4545
smut@google.comc85ac942015-09-15 16:34:43 +00004546 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004547 def pre_serialize(c):
4548 dct = c.__dict__.copy()
4549 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4550 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004551 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004552 return 0
4553
4554
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004555@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004556@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004557def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004558 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004559 parser.add_option('-d', '--display', action='store_true',
4560 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004561 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004562 help='New description to set for this issue (- for stdin, '
4563 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004564 parser.add_option('-f', '--force', action='store_true',
4565 help='Delete any unpublished Gerrit edits for this issue '
4566 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004567
4568 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004569 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004570 options, args = parser.parse_args(args)
4571 _process_codereview_select_options(parser, options)
4572
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004573 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004574 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004575 target_issue_arg = ParseIssueNumberArgument(args[0],
4576 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004577 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004578 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004579
martiniss6eda05f2016-06-30 10:18:35 -07004580 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004581 'auth_config': auth.extract_auth_config_from_options(options),
4582 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004583 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004584 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004585 if target_issue_arg:
4586 kwargs['issue'] = target_issue_arg.issue
4587 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004588 if target_issue_arg.codereview and not options.forced_codereview:
4589 detected_codereview_from_url = True
4590 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004591
4592 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004593 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004594 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004595 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004596
4597 if detected_codereview_from_url:
4598 logging.info('canonical issue/change URL: %s (type: %s)\n',
4599 cl.GetIssueURL(), target_issue_arg.codereview)
4600
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004601 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004602
smut@google.com34fb6b12015-07-13 20:03:26 +00004603 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004604 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004605 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004606
4607 if options.new_description:
4608 text = options.new_description
4609 if text == '-':
4610 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004611 elif text == '+':
4612 base_branch = cl.GetCommonAncestorWithUpstream()
4613 change = cl.GetChange(base_branch, None, local_description=True)
4614 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004615
4616 description.set_description(text)
4617 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004618 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004619
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004620 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004621 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004622 return 0
4623
4624
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004625@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004626def CMDlint(parser, args):
4627 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004628 parser.add_option('--filter', action='append', metavar='-x,+y',
4629 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004630 auth.add_auth_options(parser)
4631 options, args = parser.parse_args(args)
4632 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004633
4634 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004635 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004636 try:
4637 import cpplint
4638 import cpplint_chromium
4639 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004640 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004641 return 1
4642
4643 # Change the current working directory before calling lint so that it
4644 # shows the correct base.
4645 previous_cwd = os.getcwd()
4646 os.chdir(settings.GetRoot())
4647 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004648 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004649 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4650 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004651 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004652 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004653 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004654
4655 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004656 command = args + files
4657 if options.filter:
4658 command = ['--filter=' + ','.join(options.filter)] + command
4659 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004660
4661 white_regex = re.compile(settings.GetLintRegex())
4662 black_regex = re.compile(settings.GetLintIgnoreRegex())
4663 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4664 for filename in filenames:
4665 if white_regex.match(filename):
4666 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004667 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004668 else:
4669 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4670 extra_check_functions)
4671 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004672 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004673 finally:
4674 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004675 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004676 if cpplint._cpplint_state.error_count != 0:
4677 return 1
4678 return 0
4679
4680
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004681@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004682def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004683 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004684 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004685 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004686 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004687 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004688 parser.add_option('--all', action='store_true',
4689 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004690 parser.add_option('--parallel', action='store_true',
4691 help='Run all tests specified by input_api.RunTests in all '
4692 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004693 auth.add_auth_options(parser)
4694 options, args = parser.parse_args(args)
4695 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004696
sbc@chromium.org71437c02015-04-09 19:29:40 +00004697 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004698 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004699 return 1
4700
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004701 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004702 if args:
4703 base_branch = args[0]
4704 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004705 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004706 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004707
Aaron Gable8076c282017-11-29 14:39:41 -08004708 if options.all:
4709 base_change = cl.GetChange(base_branch, None)
4710 files = [('M', f) for f in base_change.AllFiles()]
4711 change = presubmit_support.GitChange(
4712 base_change.Name(),
4713 base_change.FullDescriptionText(),
4714 base_change.RepositoryRoot(),
4715 files,
4716 base_change.issue,
4717 base_change.patchset,
4718 base_change.author_email,
4719 base_change._upstream)
4720 else:
4721 change = cl.GetChange(base_branch, None)
4722
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004723 cl.RunHook(
4724 committing=not options.upload,
4725 may_prompt=False,
4726 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004727 change=change,
4728 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004729 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004730
4731
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004732def GenerateGerritChangeId(message):
4733 """Returns Ixxxxxx...xxx change id.
4734
4735 Works the same way as
4736 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4737 but can be called on demand on all platforms.
4738
4739 The basic idea is to generate git hash of a state of the tree, original commit
4740 message, author/committer info and timestamps.
4741 """
4742 lines = []
4743 tree_hash = RunGitSilent(['write-tree'])
4744 lines.append('tree %s' % tree_hash.strip())
4745 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4746 if code == 0:
4747 lines.append('parent %s' % parent.strip())
4748 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4749 lines.append('author %s' % author.strip())
4750 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4751 lines.append('committer %s' % committer.strip())
4752 lines.append('')
4753 # Note: Gerrit's commit-hook actually cleans message of some lines and
4754 # whitespace. This code is not doing this, but it clearly won't decrease
4755 # entropy.
4756 lines.append(message)
4757 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4758 stdin='\n'.join(lines))
4759 return 'I%s' % change_hash.strip()
4760
4761
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004762def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004763 """Computes the remote branch ref to use for the CL.
4764
4765 Args:
4766 remote (str): The git remote for the CL.
4767 remote_branch (str): The git remote branch for the CL.
4768 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004769 """
4770 if not (remote and remote_branch):
4771 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004772
wittman@chromium.org455dc922015-01-26 20:15:50 +00004773 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004774 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004775 # refs, which are then translated into the remote full symbolic refs
4776 # below.
4777 if '/' not in target_branch:
4778 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4779 else:
4780 prefix_replacements = (
4781 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4782 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4783 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4784 )
4785 match = None
4786 for regex, replacement in prefix_replacements:
4787 match = re.search(regex, target_branch)
4788 if match:
4789 remote_branch = target_branch.replace(match.group(0), replacement)
4790 break
4791 if not match:
4792 # This is a branch path but not one we recognize; use as-is.
4793 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004794 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4795 # Handle the refs that need to land in different refs.
4796 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004797
wittman@chromium.org455dc922015-01-26 20:15:50 +00004798 # Create the true path to the remote branch.
4799 # Does the following translation:
4800 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4801 # * refs/remotes/origin/master -> refs/heads/master
4802 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4803 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4804 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4805 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4806 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4807 'refs/heads/')
4808 elif remote_branch.startswith('refs/remotes/branch-heads'):
4809 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004810
wittman@chromium.org455dc922015-01-26 20:15:50 +00004811 return remote_branch
4812
4813
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004814def cleanup_list(l):
4815 """Fixes a list so that comma separated items are put as individual items.
4816
4817 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4818 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4819 """
4820 items = sum((i.split(',') for i in l), [])
4821 stripped_items = (i.strip() for i in items)
4822 return sorted(filter(None, stripped_items))
4823
4824
Aaron Gable4db38df2017-11-03 14:59:07 -07004825@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004826@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004827def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004828 """Uploads the current changelist to codereview.
4829
4830 Can skip dependency patchset uploads for a branch by running:
4831 git config branch.branch_name.skip-deps-uploads True
4832 To unset run:
4833 git config --unset branch.branch_name.skip-deps-uploads
4834 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004835
4836 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4837 a bug number, this bug number is automatically populated in the CL
4838 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004839
4840 If subject contains text in square brackets or has "<text>: " prefix, such
4841 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4842 [git-cl] add support for hashtags
4843 Foo bar: implement foo
4844 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004845 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004846 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4847 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004848 parser.add_option('--bypass-watchlists', action='store_true',
4849 dest='bypass_watchlists',
4850 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004851 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004852 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004853 parser.add_option('--message', '-m', dest='message',
4854 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004855 parser.add_option('-b', '--bug',
4856 help='pre-populate the bug number(s) for this issue. '
4857 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004858 parser.add_option('--message-file', dest='message_file',
4859 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004860 parser.add_option('--title', '-t', dest='title',
4861 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004862 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004863 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004864 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004865 parser.add_option('--tbrs',
4866 action='append', default=[],
4867 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004868 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004869 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004870 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004871 parser.add_option('--hashtag', dest='hashtags',
4872 action='append', default=[],
4873 help=('Gerrit hashtag for new CL; '
4874 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004875 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004876 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004877 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004878 help='tell the commit queue to commit this patchset; '
4879 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004880 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004881 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004882 metavar='TARGET',
4883 help='Apply CL to remote ref TARGET. ' +
4884 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004885 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004886 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004887 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004888 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004889 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004890 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004891 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4892 const='TBR', help='add a set of OWNERS to TBR')
4893 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4894 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004895 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4896 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004897 help='Send the patchset to do a CQ dry run right after '
4898 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004899 parser.add_option('--dependencies', action='store_true',
4900 help='Uploads CLs of all the local branches that depend on '
4901 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004902 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4903 help='Sends your change to the CQ after an approval. Only '
4904 'works on repos that have the Auto-Submit label '
4905 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004906 parser.add_option('--parallel', action='store_true',
4907 help='Run all tests specified by input_api.RunTests in all '
4908 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004909
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004910 parser.add_option('--no-autocc', action='store_true',
4911 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004912 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004913 help='Set the review private. This implies --no-autocc.')
4914
rmistry@google.com2dd99862015-06-22 12:22:18 +00004915 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004916 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004917 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004918 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004919 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004920 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004921
sbc@chromium.org71437c02015-04-09 19:29:40 +00004922 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004923 return 1
4924
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004925 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004926 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004927 options.cc = cleanup_list(options.cc)
4928
tandriib80458a2016-06-23 12:20:07 -07004929 if options.message_file:
4930 if options.message:
4931 parser.error('only one of --message and --message-file allowed.')
4932 options.message = gclient_utils.FileRead(options.message_file)
4933 options.message_file = None
4934
tandrii4d0545a2016-07-06 03:56:49 -07004935 if options.cq_dry_run and options.use_commit_queue:
4936 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4937
Aaron Gableedbc4132017-09-11 13:22:28 -07004938 if options.use_commit_queue:
4939 options.send_mail = True
4940
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004941 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4942 settings.GetIsGerrit()
4943
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004944 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004945 if not cl.IsGerrit():
4946 # Error out with instructions for repos not yet configured for Gerrit.
4947 print('=====================================')
4948 print('NOTICE: Rietveld is no longer supported. '
4949 'You can upload changes to Gerrit with')
4950 print(' git cl upload --gerrit')
4951 print('or set Gerrit to be your default code review tool with')
4952 print(' git config gerrit.host true')
4953 print('=====================================')
4954 return 1
4955
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004956 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004957
4958
Francois Dorayd42c6812017-05-30 15:10:20 -04004959@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004960@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004961def CMDsplit(parser, args):
4962 """Splits a branch into smaller branches and uploads CLs.
4963
4964 Creates a branch and uploads a CL for each group of files modified in the
4965 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004966 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004967 the shared OWNERS file.
4968 """
4969 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004970 help="A text file containing a CL description in which "
4971 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004972 parser.add_option("-c", "--comment", dest="comment_file",
4973 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004974 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4975 default=False,
4976 help="List the files and reviewers for each CL that would "
4977 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004978 parser.add_option("--cq-dry-run", action='store_true',
4979 help="If set, will do a cq dry run for each uploaded CL. "
4980 "Please be careful when doing this; more than ~10 CLs "
4981 "has the potential to overload our build "
4982 "infrastructure. Try to upload these not during high "
4983 "load times (usually 11-3 Mountain View time). Email "
4984 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004985 options, _ = parser.parse_args(args)
4986
4987 if not options.description_file:
4988 parser.error('No --description flag specified.')
4989
4990 def WrappedCMDupload(args):
4991 return CMDupload(OptionParser(), args)
4992
4993 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004994 Changelist, WrappedCMDupload, options.dry_run,
4995 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04004996
4997
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004998@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004999@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005000def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005001 """DEPRECATED: Used to commit the current changelist via git-svn."""
5002 message = ('git-cl no longer supports committing to SVN repositories via '
5003 'git-svn. You probably want to use `git cl land` instead.')
5004 print(message)
5005 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005006
5007
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005008# Two special branches used by git cl land.
5009MERGE_BRANCH = 'git-cl-commit'
5010CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5011
5012
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005013@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005014@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005015def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005016 """Commits the current changelist via git.
5017
5018 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5019 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005020 """
5021 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5022 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07005023 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005024 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005025 parser.add_option('--parallel', action='store_true',
5026 help='Run all tests specified by input_api.RunTests in all '
5027 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005028 auth.add_auth_options(parser)
5029 (options, args) = parser.parse_args(args)
5030 auth_config = auth.extract_auth_config_from_options(options)
5031
5032 cl = Changelist(auth_config=auth_config)
5033
Robert Iannucci2e73d432018-03-14 01:10:47 -07005034 if not cl.IsGerrit():
5035 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005036
Robert Iannucci2e73d432018-03-14 01:10:47 -07005037 if not cl.GetIssue():
5038 DieWithError('You must upload the change first to Gerrit.\n'
5039 ' If you would rather have `git cl land` upload '
5040 'automatically for you, see http://crbug.com/642759')
5041 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005042 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005043
5044
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005045@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005046@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005047def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005048 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005049 parser.add_option('-b', dest='newbranch',
5050 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005051 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005052 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005053 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005054 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005055 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005056 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005057 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005058 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005059 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005060 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005061
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005062
5063 group = optparse.OptionGroup(
5064 parser,
5065 'Options for continuing work on the current issue uploaded from a '
5066 'different clone (e.g. different machine). Must be used independently '
5067 'from the other options. No issue number should be specified, and the '
5068 'branch must have an issue number associated with it')
5069 group.add_option('--reapply', action='store_true', dest='reapply',
5070 help='Reset the branch and reapply the issue.\n'
5071 'CAUTION: This will undo any local changes in this '
5072 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005073
5074 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005075 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005076 parser.add_option_group(group)
5077
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005078 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005079 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005080 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005081 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005082 auth_config = auth.extract_auth_config_from_options(options)
5083
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005084 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005085 if options.newbranch:
5086 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005087 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005088 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005089
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005090 cl = Changelist(auth_config=auth_config,
5091 codereview=options.forced_codereview)
5092 if not cl.GetIssue():
5093 parser.error('current branch must have an associated issue')
5094
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005095 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005096 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005097 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005098
5099 RunGit(['reset', '--hard', upstream])
5100 if options.pull:
5101 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005102
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005103 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5104 options.directory)
5105
5106 if len(args) != 1 or not args[0]:
5107 parser.error('Must specify issue number or url')
5108
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005109 target_issue_arg = ParseIssueNumberArgument(args[0],
5110 options.forced_codereview)
5111 if not target_issue_arg.valid:
5112 parser.error('invalid codereview url or CL id')
5113
5114 cl_kwargs = {
5115 'auth_config': auth_config,
5116 'codereview_host': target_issue_arg.hostname,
5117 'codereview': options.forced_codereview,
5118 }
5119 detected_codereview_from_url = False
5120 if target_issue_arg.codereview and not options.forced_codereview:
5121 detected_codereview_from_url = True
5122 cl_kwargs['codereview'] = target_issue_arg.codereview
5123 cl_kwargs['issue'] = target_issue_arg.issue
5124
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005125 # We don't want uncommitted changes mixed up with the patch.
5126 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005127 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005128
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005129 if options.newbranch:
5130 if options.force:
5131 RunGit(['branch', '-D', options.newbranch],
5132 stderr=subprocess2.PIPE, error_ok=True)
5133 RunGit(['new-branch', options.newbranch])
5134
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005135 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005136
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005137 if cl.IsGerrit():
5138 if options.reject:
5139 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005140 if options.directory:
5141 parser.error('--directory is not supported with Gerrit codereview.')
5142
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005143 if detected_codereview_from_url:
5144 print('canonical issue/change URL: %s (type: %s)\n' %
5145 (cl.GetIssueURL(), target_issue_arg.codereview))
5146
5147 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005148 options.nocommit, options.directory,
5149 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005150
5151
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005152def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005153 """Fetches the tree status and returns either 'open', 'closed',
5154 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005155 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005156 if url:
5157 status = urllib2.urlopen(url).read().lower()
5158 if status.find('closed') != -1 or status == '0':
5159 return 'closed'
5160 elif status.find('open') != -1 or status == '1':
5161 return 'open'
5162 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005163 return 'unset'
5164
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005165
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005166def GetTreeStatusReason():
5167 """Fetches the tree status from a json url and returns the message
5168 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005169 url = settings.GetTreeStatusUrl()
5170 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005171 connection = urllib2.urlopen(json_url)
5172 status = json.loads(connection.read())
5173 connection.close()
5174 return status['message']
5175
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005176
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005177@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005178def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005179 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005180 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005181 status = GetTreeStatus()
5182 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005183 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005184 return 2
5185
vapiera7fbd5a2016-06-16 09:17:49 -07005186 print('The tree is %s' % status)
5187 print()
5188 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005189 if status != 'open':
5190 return 1
5191 return 0
5192
5193
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005194@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005195def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005196 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005197 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005198 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005199 '-b', '--bot', action='append',
5200 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5201 'times to specify multiple builders. ex: '
5202 '"-b win_rel -b win_layout". See '
5203 'the try server waterfall for the builders name and the tests '
5204 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005205 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005206 '-B', '--bucket', default='',
5207 help=('Buildbucket bucket to send the try requests.'))
5208 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005209 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005210 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005211 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005212 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005213 help='Revision to use for the try job; default: the revision will '
5214 'be determined by the try recipe that builder runs, which usually '
5215 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005216 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005217 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005218 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005219 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005220 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005221 '--category', default='git_cl_try', help='Specify custom build category.')
5222 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005223 '--project',
5224 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005225 'in recipe to determine to which repository or directory to '
5226 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005227 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005228 '-p', '--property', dest='properties', action='append', default=[],
5229 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005230 'key2=value2 etc. The value will be treated as '
5231 'json if decodable, or as string otherwise. '
5232 'NOTE: using this may make your try job not usable for CQ, '
5233 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005234 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005235 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5236 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005237 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005238 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005239 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005240 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005241 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005242 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005243
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005244 if options.master and options.master.startswith('luci.'):
5245 parser.error(
5246 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005247 # Make sure that all properties are prop=value pairs.
5248 bad_params = [x for x in options.properties if '=' not in x]
5249 if bad_params:
5250 parser.error('Got properties with missing "=": %s' % bad_params)
5251
maruel@chromium.org15192402012-09-06 12:38:29 +00005252 if args:
5253 parser.error('Unknown arguments: %s' % args)
5254
Koji Ishii31c14782018-01-08 17:17:33 +09005255 cl = Changelist(auth_config=auth_config, issue=options.issue,
5256 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005257 if not cl.GetIssue():
5258 parser.error('Need to upload first')
5259
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005260 if cl.IsGerrit():
5261 # HACK: warm up Gerrit change detail cache to save on RPCs.
5262 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5263
tandriie113dfd2016-10-11 10:20:12 -07005264 error_message = cl.CannotTriggerTryJobReason()
5265 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005266 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005267
borenet6c0efe62016-10-19 08:13:29 -07005268 if options.bucket and options.master:
5269 parser.error('Only one of --bucket and --master may be used.')
5270
qyearsley1fdfcb62016-10-24 13:22:03 -07005271 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005272
qyearsleydd49f942016-10-28 11:57:22 -07005273 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5274 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005275 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005276 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005277 print('git cl try with no bots now defaults to CQ dry run.')
5278 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5279 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005280
borenet6c0efe62016-10-19 08:13:29 -07005281 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005282 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005283 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005284 'of bot requires an initial job from a parent (usually a builder). '
5285 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005286 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005287 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005288
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005289 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005290 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005291 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005292 except BuildbucketResponseException as ex:
5293 print('ERROR: %s' % ex)
5294 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005295 return 0
5296
5297
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005298@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005299def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005300 """Prints info about try jobs associated with current CL."""
5301 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005302 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005303 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005304 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005305 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005306 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005307 '--color', action='store_true', default=setup_color.IS_TTY,
5308 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005309 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005310 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5311 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005312 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005313 '--json', help=('Path of JSON output file to write try job results to,'
5314 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005315 parser.add_option_group(group)
5316 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005317 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005318 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005319 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005320 if args:
5321 parser.error('Unrecognized args: %s' % ' '.join(args))
5322
5323 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005324 cl = Changelist(
5325 issue=options.issue, codereview=options.forced_codereview,
5326 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005327 if not cl.GetIssue():
5328 parser.error('Need to upload first')
5329
tandrii221ab252016-10-06 08:12:04 -07005330 patchset = options.patchset
5331 if not patchset:
5332 patchset = cl.GetMostRecentPatchset()
5333 if not patchset:
5334 parser.error('Codereview doesn\'t know about issue %s. '
5335 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005336 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005337 cl.GetIssue())
5338
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005339 try:
tandrii221ab252016-10-06 08:12:04 -07005340 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005341 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005342 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005343 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005344 if options.json:
5345 write_try_results_json(options.json, jobs)
5346 else:
5347 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005348 return 0
5349
5350
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005351@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005352@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005353def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005354 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005355 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005356 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005357 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005358
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005359 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005360 if args:
5361 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005362 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005363 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005364 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005365 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005366
5367 # Clear configured merge-base, if there is one.
5368 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005369 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005370 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005371 return 0
5372
5373
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005374@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005375def CMDweb(parser, args):
5376 """Opens the current CL in the web browser."""
5377 _, args = parser.parse_args(args)
5378 if args:
5379 parser.error('Unrecognized args: %s' % ' '.join(args))
5380
5381 issue_url = Changelist().GetIssueURL()
5382 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005383 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005384 return 1
5385
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005386 # Redirect I/O before invoking browser to hide its output. For example, this
5387 # allows to hide "Created new window in existing browser session." message
5388 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5389 saved_stdout = os.dup(1)
5390 os.close(1)
5391 os.open(os.devnull, os.O_RDWR)
5392 try:
5393 webbrowser.open(issue_url)
5394 finally:
5395 os.dup2(saved_stdout, 1)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005396 return 0
5397
5398
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005399@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005400def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005401 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005402 parser.add_option('-d', '--dry-run', action='store_true',
5403 help='trigger in dry run mode')
5404 parser.add_option('-c', '--clear', action='store_true',
5405 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005406 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005407 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005408 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005409 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005410 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005411 if args:
5412 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005413 if options.dry_run and options.clear:
5414 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5415
iannuccie53c9352016-08-17 14:40:40 -07005416 cl = Changelist(auth_config=auth_config, issue=options.issue,
5417 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005418 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005419 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005420 elif options.dry_run:
5421 state = _CQState.DRY_RUN
5422 else:
5423 state = _CQState.COMMIT
5424 if not cl.GetIssue():
5425 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005426 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005427 return 0
5428
5429
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005430@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005431def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005432 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005433 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005434 auth.add_auth_options(parser)
5435 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005436 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005437 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005438 if args:
5439 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005440 cl = Changelist(auth_config=auth_config, issue=options.issue,
5441 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005442 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005443 if not cl.GetIssue():
5444 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005445 cl.CloseIssue()
5446 return 0
5447
5448
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005449@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005450def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005451 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005452 parser.add_option(
5453 '--stat',
5454 action='store_true',
5455 dest='stat',
5456 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005457 auth.add_auth_options(parser)
5458 options, args = parser.parse_args(args)
5459 auth_config = auth.extract_auth_config_from_options(options)
5460 if args:
5461 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005462
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005463 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005464 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005465 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005466 if not issue:
5467 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005468
Aaron Gablea718c3e2017-08-28 17:47:28 -07005469 base = cl._GitGetBranchConfigValue('last-upload-hash')
5470 if not base:
5471 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5472 if not base:
5473 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5474 revision_info = detail['revisions'][detail['current_revision']]
5475 fetch_info = revision_info['fetch']['http']
5476 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5477 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005478
Aaron Gablea718c3e2017-08-28 17:47:28 -07005479 cmd = ['git', 'diff']
5480 if options.stat:
5481 cmd.append('--stat')
5482 cmd.append(base)
5483 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005484
5485 return 0
5486
5487
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005488@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005489def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005490 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005491 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005492 '--ignore-current',
5493 action='store_true',
5494 help='Ignore the CL\'s current reviewers and start from scratch.')
5495 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005496 '--no-color',
5497 action='store_true',
5498 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005499 parser.add_option(
5500 '--batch',
5501 action='store_true',
5502 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005503 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005504 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005505 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005506
5507 author = RunGit(['config', 'user.email']).strip() or None
5508
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005509 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005510
5511 if args:
5512 if len(args) > 1:
5513 parser.error('Unknown args')
5514 base_branch = args[0]
5515 else:
5516 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005517 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005518
5519 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005520 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5521
5522 if options.batch:
5523 db = owners.Database(change.RepositoryRoot(), file, os.path)
5524 print('\n'.join(db.reviewers_for(affected_files, author)))
5525 return 0
5526
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005527 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005528 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005529 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005530 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005531 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005532 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005533 disable_color=options.no_color,
5534 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005535
5536
Aiden Bennerc08566e2018-10-03 17:52:42 +00005537def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005538 """Generates a diff command."""
5539 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005540 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5541
5542 if not allow_prefix:
5543 diff_cmd += ['--no-prefix']
5544
5545 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005546
5547 if args:
5548 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005549 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005550 diff_cmd.append(arg)
5551 else:
5552 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005553
5554 return diff_cmd
5555
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005556
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005557def MatchingFileType(file_name, extensions):
5558 """Returns true if the file name ends with one of the given extensions."""
5559 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005560
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005561
enne@chromium.org555cfe42014-01-29 18:21:39 +00005562@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005563@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005564def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005565 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005566 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005567 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005568 parser.add_option('--full', action='store_true',
5569 help='Reformat the full content of all touched files')
5570 parser.add_option('--dry-run', action='store_true',
5571 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005572 parser.add_option('--python', action='store_true',
5573 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005574 parser.add_option('--js', action='store_true',
5575 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005576 parser.add_option('--diff', action='store_true',
5577 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005578 parser.add_option('--presubmit', action='store_true',
5579 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005580 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005581
Daniel Chengc55eecf2016-12-30 03:11:02 -08005582 # Normalize any remaining args against the current path, so paths relative to
5583 # the current directory are still resolved as expected.
5584 args = [os.path.join(os.getcwd(), arg) for arg in args]
5585
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005586 # git diff generates paths against the root of the repository. Change
5587 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005588 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005589 if rel_base_path:
5590 os.chdir(rel_base_path)
5591
digit@chromium.org29e47272013-05-17 17:01:46 +00005592 # Grab the merge-base commit, i.e. the upstream commit of the current
5593 # branch when it was created or the last time it was rebased. This is
5594 # to cover the case where the user may have called "git fetch origin",
5595 # moving the origin branch to a newer commit, but hasn't rebased yet.
5596 upstream_commit = None
5597 cl = Changelist()
5598 upstream_branch = cl.GetUpstreamBranch()
5599 if upstream_branch:
5600 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5601 upstream_commit = upstream_commit.strip()
5602
5603 if not upstream_commit:
5604 DieWithError('Could not find base commit for this branch. '
5605 'Are you in detached state?')
5606
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005607 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5608 diff_output = RunGit(changed_files_cmd)
5609 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005610 # Filter out files deleted by this CL
5611 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005612
Christopher Lamc5ba6922017-01-24 11:19:14 +11005613 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005614 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005615
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005616 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5617 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5618 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005619 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005620
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005621 top_dir = os.path.normpath(
5622 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5623
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005624 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5625 # formatted. This is used to block during the presubmit.
5626 return_value = 0
5627
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005628 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005629 # Locate the clang-format binary in the checkout
5630 try:
5631 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005632 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005633 DieWithError(e)
5634
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005635 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005636 cmd = [clang_format_tool]
5637 if not opts.dry_run and not opts.diff:
5638 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005639 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005640 if opts.diff:
5641 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005642 else:
5643 env = os.environ.copy()
5644 env['PATH'] = str(os.path.dirname(clang_format_tool))
5645 try:
5646 script = clang_format.FindClangFormatScriptInChromiumTree(
5647 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005648 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005649 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005650
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005651 cmd = [sys.executable, script, '-p0']
5652 if not opts.dry_run and not opts.diff:
5653 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005654
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005655 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5656 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005657
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005658 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5659 if opts.diff:
5660 sys.stdout.write(stdout)
5661 if opts.dry_run and len(stdout) > 0:
5662 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005663
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005664 # Similar code to above, but using yapf on .py files rather than clang-format
5665 # on C/C++ files
Aiden Bennerc08566e2018-10-03 17:52:42 +00005666 if opts.python and python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005667 yapf_tool = gclient_utils.FindExecutable('yapf')
5668 if yapf_tool is None:
5669 DieWithError('yapf not found in PATH')
5670
Aiden Bennerc08566e2018-10-03 17:52:42 +00005671 # If we couldn't find a yapf file we'll default to the chromium style
5672 # specified in depot_tools.
5673 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5674 chromium_default_yapf_style = os.path.join(depot_tools_path,
5675 YAPF_CONFIG_FILENAME)
5676
5677 # Note: yapf still seems to fix indentation of the entire file
5678 # even if line ranges are specified.
5679 # See https://github.com/google/yapf/issues/499
5680 if not opts.full:
5681 py_line_diffs = _ComputeDiffLineRanges(python_diff_files, upstream_commit)
5682
5683 # Used for caching.
5684 yapf_configs = {}
5685 for f in python_diff_files:
5686 # Find the yapf style config for the current file, defaults to depot
5687 # tools default.
5688 yapf_config = _FindYapfConfigFile(
5689 os.path.abspath(f), yapf_configs, top_dir,
5690 chromium_default_yapf_style)
5691
5692 cmd = [yapf_tool, '--style', yapf_config, f]
5693
5694 has_formattable_lines = False
5695 if not opts.full:
5696 # Only run yapf over changed line ranges.
5697 for diff_start, diff_len in py_line_diffs[f]:
5698 diff_end = diff_start + diff_len - 1
5699 # Yapf errors out if diff_end < diff_start but this
5700 # is a valid line range diff for a removal.
5701 if diff_end >= diff_start:
5702 has_formattable_lines = True
5703 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5704 # If all line diffs were removals we have nothing to format.
5705 if not has_formattable_lines:
5706 continue
5707
5708 if opts.diff or opts.dry_run:
5709 cmd += ['--diff']
5710 # Will return non-zero exit code if non-empty diff.
5711 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5712 if opts.diff:
5713 sys.stdout.write(stdout)
5714 elif len(stdout) > 0:
5715 return_value = 2
5716 else:
5717 cmd += ['-i']
5718 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005719
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005720 # Dart's formatter does not have the nice property of only operating on
5721 # modified chunks, so hard code full.
5722 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005723 try:
5724 command = [dart_format.FindDartFmtToolInChromiumTree()]
5725 if not opts.dry_run and not opts.diff:
5726 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005727 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005728
ppi@chromium.org6593d932016-03-03 15:41:15 +00005729 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005730 if opts.dry_run and stdout:
5731 return_value = 2
5732 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005733 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5734 'found in this checkout. Files in other languages are still '
5735 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005736
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005737 # Format GN build files. Always run on full build files for canonical form.
5738 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005739 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005740 if opts.dry_run or opts.diff:
5741 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005742 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005743 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5744 shell=sys.platform == 'win32',
5745 cwd=top_dir)
5746 if opts.dry_run and gn_ret == 2:
5747 return_value = 2 # Not formatted.
5748 elif opts.diff and gn_ret == 2:
5749 # TODO this should compute and print the actual diff.
5750 print("This change has GN build file diff for " + gn_diff_file)
5751 elif gn_ret != 0:
5752 # For non-dry run cases (and non-2 return values for dry-run), a
5753 # nonzero error code indicates a failure, probably because the file
5754 # doesn't parse.
5755 DieWithError("gn format failed on " + gn_diff_file +
5756 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005757
Ilya Shermane081cbe2017-08-15 17:51:04 -07005758 # Skip the metrics formatting from the global presubmit hook. These files have
5759 # a separate presubmit hook that issues an error if the files need formatting,
5760 # whereas the top-level presubmit script merely issues a warning. Formatting
5761 # these files is somewhat slow, so it's important not to duplicate the work.
5762 if not opts.presubmit:
5763 for xml_dir in GetDirtyMetricsDirs(diff_files):
5764 tool_dir = os.path.join(top_dir, xml_dir)
5765 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5766 if opts.dry_run or opts.diff:
5767 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005768 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005769 if opts.diff:
5770 sys.stdout.write(stdout)
5771 if opts.dry_run and stdout:
5772 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005773
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005774 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005775
Steven Holte2e664bf2017-04-21 13:10:47 -07005776def GetDirtyMetricsDirs(diff_files):
5777 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5778 metrics_xml_dirs = [
5779 os.path.join('tools', 'metrics', 'actions'),
5780 os.path.join('tools', 'metrics', 'histograms'),
5781 os.path.join('tools', 'metrics', 'rappor'),
5782 os.path.join('tools', 'metrics', 'ukm')]
5783 for xml_dir in metrics_xml_dirs:
5784 if any(file.startswith(xml_dir) for file in xml_diff_files):
5785 yield xml_dir
5786
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005787
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005788@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005789@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005790def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005791 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005792 _, args = parser.parse_args(args)
5793
5794 if len(args) != 1:
5795 parser.print_help()
5796 return 1
5797
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005798 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005799 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005800 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005801
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005802 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005803
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005804 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005805 output = RunGit(['config', '--local', '--get-regexp',
5806 r'branch\..*\.%s' % issueprefix],
5807 error_ok=True)
5808 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005809 if issue == target_issue:
5810 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005811
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005812 branches = []
5813 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005814 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005815 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005816 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005817 return 1
5818 if len(branches) == 1:
5819 RunGit(['checkout', branches[0]])
5820 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005821 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005822 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005823 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005824 which = raw_input('Choose by index: ')
5825 try:
5826 RunGit(['checkout', branches[int(which)]])
5827 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005828 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005829 return 1
5830
5831 return 0
5832
5833
maruel@chromium.org29404b52014-09-08 22:58:00 +00005834def CMDlol(parser, args):
5835 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005836 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005837 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5838 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5839 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005840 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005841 return 0
5842
5843
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005844class OptionParser(optparse.OptionParser):
5845 """Creates the option parse and add --verbose support."""
5846 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005847 optparse.OptionParser.__init__(
5848 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005849 self.add_option(
5850 '-v', '--verbose', action='count', default=0,
5851 help='Use 2 times for more debugging info')
5852
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005853 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005854 try:
5855 return self._parse_args(args)
5856 finally:
5857 # Regardless of success or failure of args parsing, we want to report
5858 # metrics, but only after logging has been initialized (if parsing
5859 # succeeded).
5860 global settings
5861 settings = Settings()
5862
5863 if not metrics.DISABLE_METRICS_COLLECTION:
5864 # GetViewVCUrl ultimately calls logging method.
5865 project_url = settings.GetViewVCUrl().strip('/+')
5866 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5867 metrics.collector.add('project_urls', [project_url])
5868
5869 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005870 # Create an optparse.Values object that will store only the actual passed
5871 # options, without the defaults.
5872 actual_options = optparse.Values()
5873 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5874 # Create an optparse.Values object with the default options.
5875 options = optparse.Values(self.get_default_values().__dict__)
5876 # Update it with the options passed by the user.
5877 options._update_careful(actual_options.__dict__)
5878 # Store the options passed by the user in an _actual_options attribute.
5879 # We store only the keys, and not the values, since the values can contain
5880 # arbitrary information, which might be PII.
5881 metrics.collector.add('arguments', actual_options.__dict__.keys())
5882
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005883 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005884 logging.basicConfig(
5885 level=levels[min(options.verbose, len(levels) - 1)],
5886 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5887 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005888
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005889 return options, args
5890
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005891
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005892def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005893 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005894 print('\nYour python version %s is unsupported, please upgrade.\n' %
5895 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005896 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005897
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005898 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005899 dispatcher = subcommand.CommandDispatcher(__name__)
5900 try:
5901 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005902 except auth.AuthenticationError as e:
5903 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005904 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005905 if e.code != 500:
5906 raise
5907 DieWithError(
5908 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5909 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005910 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005911
5912
5913if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005914 # These affect sys.stdout so do it outside of main() to simplify mocks in
5915 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005916 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005917 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005918 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005919 sys.exit(main(sys.argv[1:]))