blob: 043717ba6de39253349700ac6e2d2e74f8c12c40 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000032import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000033import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000035import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000036import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000037import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000038import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000039
40try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080041 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042except ImportError:
43 pass
44
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000045from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000046from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000058import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000059import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000060import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000061import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import presubmit_support
63import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040064import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067import watchlists
68
tandrii7400cf02016-06-21 08:48:07 -070069__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000070
tandrii9d2c7a32016-06-22 03:42:45 -070071COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070072DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080073POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000075REFS_THAT_ALIAS_TO_OTHER_REFS = {
76 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
77 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
78}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079
thestig@chromium.org44202a22014-03-11 19:22:18 +000080# Valid extensions for files we want to lint.
81DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
82DEFAULT_LINT_IGNORE_REGEX = r"$^"
83
Aiden Bennerc08566e2018-10-03 17:52:42 +000084# File name for yapf style config files.
85YAPF_CONFIG_FILENAME = '.style.yapf'
86
borenet6c0efe62016-10-19 08:13:29 -070087# Buildbucket master name prefix.
88MASTER_PREFIX = 'master.'
89
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000090# Shortcut since it quickly becomes redundant.
91Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000092
maruel@chromium.orgddd59412011-11-30 14:20:38 +000093# Initialized in main()
94settings = None
95
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010096# Used by tests/git_cl_test.py to add extra logging.
97# Inside the weirdly failing test, add this:
98# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -070099# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100100_IS_BEING_TESTED = False
101
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000102
Christopher Lamf732cd52017-01-24 12:40:11 +1100103def DieWithError(message, change_desc=None):
104 if change_desc:
105 SaveDescriptionBackup(change_desc)
106
vapiera7fbd5a2016-06-16 09:17:49 -0700107 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108 sys.exit(1)
109
110
Christopher Lamf732cd52017-01-24 12:40:11 +1100111def SaveDescriptionBackup(change_desc):
112 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000113 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100114 backup_file = open(backup_path, 'w')
115 backup_file.write(change_desc.description)
116 backup_file.close()
117
118
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000119def GetNoGitPagerEnv():
120 env = os.environ.copy()
121 # 'cat' is a magical git string that disables pagers on all platforms.
122 env['GIT_PAGER'] = 'cat'
123 return env
124
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000125
bsep@chromium.org627d9002016-04-29 00:00:52 +0000126def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000127 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000128 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000129 except subprocess2.CalledProcessError as e:
130 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000131 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000132 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000133 'Command "%s" failed.\n%s' % (
134 ' '.join(args), error_message or e.stdout or ''))
135 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000136
137
138def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000139 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000140 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000141
142
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000143def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000144 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700145 if suppress_stderr:
146 stderr = subprocess2.VOID
147 else:
148 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000149 try:
tandrii5d48c322016-08-18 16:19:37 -0700150 (out, _), code = subprocess2.communicate(['git'] + args,
151 env=GetNoGitPagerEnv(),
152 stdout=subprocess2.PIPE,
153 stderr=stderr)
154 return code, out
155 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900156 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700157 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000158
159
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000160def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000161 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000162 return RunGitWithCode(args, suppress_stderr=True)[1]
163
164
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000165def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000166 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000167 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000168 return (version.startswith(prefix) and
169 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000170
171
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000172def BranchExists(branch):
173 """Return True if specified branch exists."""
174 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
175 suppress_stderr=True)
176 return not code
177
178
tandrii2a16b952016-10-19 07:09:44 -0700179def time_sleep(seconds):
180 # Use this so that it can be mocked in tests without interfering with python
181 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700182 return time.sleep(seconds)
183
184
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000185def time_time():
186 # Use this so that it can be mocked in tests without interfering with python
187 # system machinery.
188 return time.time()
189
190
maruel@chromium.org90541732011-04-01 17:54:18 +0000191def ask_for_data(prompt):
192 try:
193 return raw_input(prompt)
194 except KeyboardInterrupt:
195 # Hide the exception.
196 sys.exit(1)
197
198
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100199def confirm_or_exit(prefix='', action='confirm'):
200 """Asks user to press enter to continue or press Ctrl+C to abort."""
201 if not prefix or prefix.endswith('\n'):
202 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100203 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100204 mid = ' Press'
205 elif prefix.endswith(' '):
206 mid = 'press'
207 else:
208 mid = ' press'
209 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
210
211
212def ask_for_explicit_yes(prompt):
213 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
214 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
215 while True:
216 if 'yes'.startswith(result):
217 return True
218 if 'no'.startswith(result):
219 return False
220 result = ask_for_data('Please, type yes or no: ').lower()
221
222
tandrii5d48c322016-08-18 16:19:37 -0700223def _git_branch_config_key(branch, key):
224 """Helper method to return Git config key for a branch."""
225 assert branch, 'branch name is required to set git config for it'
226 return 'branch.%s.%s' % (branch, key)
227
228
229def _git_get_branch_config_value(key, default=None, value_type=str,
230 branch=False):
231 """Returns git config value of given or current branch if any.
232
233 Returns default in all other cases.
234 """
235 assert value_type in (int, str, bool)
236 if branch is False: # Distinguishing default arg value from None.
237 branch = GetCurrentBranch()
238
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000239 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700240 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000241
tandrii5d48c322016-08-18 16:19:37 -0700242 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700243 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700244 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700245 # git config also has --int, but apparently git config suffers from integer
246 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700247 args.append(_git_branch_config_key(branch, key))
248 code, out = RunGitWithCode(args)
249 if code == 0:
250 value = out.strip()
251 if value_type == int:
252 return int(value)
253 if value_type == bool:
254 return bool(value.lower() == 'true')
255 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000256 return default
257
258
tandrii5d48c322016-08-18 16:19:37 -0700259def _git_set_branch_config_value(key, value, branch=None, **kwargs):
260 """Sets the value or unsets if it's None of a git branch config.
261
262 Valid, though not necessarily existing, branch must be provided,
263 otherwise currently checked out branch is used.
264 """
265 if not branch:
266 branch = GetCurrentBranch()
267 assert branch, 'a branch name OR currently checked out branch is required'
268 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700269 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700270 if value is None:
271 args.append('--unset')
272 elif isinstance(value, bool):
273 args.append('--bool')
274 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700275 else:
tandrii33a46ff2016-08-23 05:53:40 -0700276 # git config also has --int, but apparently git config suffers from integer
277 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700278 value = str(value)
279 args.append(_git_branch_config_key(branch, key))
280 if value is not None:
281 args.append(value)
282 RunGit(args, **kwargs)
283
284
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100285def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700286 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100287
288 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
289 """
290 # Git also stores timezone offset, but it only affects visual display,
291 # actual point in time is defined by this timestamp only.
292 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
293
294
295def _git_amend_head(message, committer_timestamp):
296 """Amends commit with new message and desired committer_timestamp.
297
298 Sets committer timezone to UTC.
299 """
300 env = os.environ.copy()
301 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
302 return RunGit(['commit', '--amend', '-m', message], env=env)
303
304
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305def _get_properties_from_options(options):
306 properties = dict(x.split('=', 1) for x in options.properties)
307 for key, val in properties.iteritems():
308 try:
309 properties[key] = json.loads(val)
310 except ValueError:
311 pass # If a value couldn't be evaluated, treat it as a string.
312 return properties
313
314
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000315def _prefix_master(master):
316 """Convert user-specified master name to full master name.
317
318 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
319 name, while the developers always use shortened master name
320 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
321 function does the conversion for buildbucket migration.
322 """
borenet6c0efe62016-10-19 08:13:29 -0700323 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000324 return master
borenet6c0efe62016-10-19 08:13:29 -0700325 return '%s%s' % (MASTER_PREFIX, master)
326
327
328def _unprefix_master(bucket):
329 """Convert bucket name to shortened master name.
330
331 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
332 name, while the developers always use shortened master name
333 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
334 function does the conversion for buildbucket migration.
335 """
336 if bucket.startswith(MASTER_PREFIX):
337 return bucket[len(MASTER_PREFIX):]
338 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339
340
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000341def _buildbucket_retry(operation_name, http, *args, **kwargs):
342 """Retries requests to buildbucket service and returns parsed json content."""
343 try_count = 0
344 while True:
345 response, content = http.request(*args, **kwargs)
346 try:
347 content_json = json.loads(content)
348 except ValueError:
349 content_json = None
350
351 # Buildbucket could return an error even if status==200.
352 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000353 error = content_json.get('error')
354 if error.get('code') == 403:
355 raise BuildbucketResponseException(
356 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000357 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000358 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 raise BuildbucketResponseException(msg)
360
361 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700362 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000363 raise BuildbucketResponseException(
364 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700365 'Please file bugs at http://crbug.com, '
366 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000367 content)
368 return content_json
369 if response.status < 500 or try_count >= 2:
370 raise httplib2.HttpLib2Error(content)
371
372 # status >= 500 means transient failures.
373 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700374 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000375 try_count += 1
376 assert False, 'unreachable'
377
378
qyearsley1fdfcb62016-10-24 13:22:03 -0700379def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700380 """Returns a dict mapping bucket names to builders and tests,
381 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700382 """
qyearsleydd49f942016-10-28 11:57:22 -0700383 # If no bots are listed, we try to get a set of builders and tests based
384 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700385 if not options.bot:
386 change = changelist.GetChange(
387 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700388 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700389 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700390 change=change,
391 changed_files=change.LocalPaths(),
392 repository_root=settings.GetRoot(),
393 default_presubmit=None,
394 project=None,
395 verbose=options.verbose,
396 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700397 if masters is None:
398 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100399 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700400
qyearsley1fdfcb62016-10-24 13:22:03 -0700401 if options.bucket:
402 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700403 if options.master:
404 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700405
qyearsleydd49f942016-10-28 11:57:22 -0700406 # If bots are listed but no master or bucket, then we need to find out
407 # the corresponding master for each bot.
408 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
409 if error_message:
410 option_parser.error(
411 'Tryserver master cannot be found because: %s\n'
412 'Please manually specify the tryserver master, e.g. '
413 '"-m tryserver.chromium.linux".' % error_message)
414 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700415
416
qyearsley123a4682016-10-26 09:12:17 -0700417def _get_bucket_map_for_builders(builders):
418 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 map_url = 'https://builders-map.appspot.com/'
420 try:
qyearsley123a4682016-10-26 09:12:17 -0700421 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700422 except urllib2.URLError as e:
423 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
424 (map_url, e))
425 except ValueError as e:
426 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700427 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700428 return None, 'Failed to build master map.'
429
qyearsley123a4682016-10-26 09:12:17 -0700430 bucket_map = {}
431 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800432 bucket = builders_map.get(builder, {}).get('bucket')
433 if bucket:
434 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700435 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700436
437
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800438def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700439 """Sends a request to Buildbucket to trigger try jobs for a changelist.
440
441 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700442 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700443 changelist: Changelist that the try jobs are associated with.
444 buckets: A nested dict mapping bucket names to builders to tests.
445 options: Command-line options.
446 """
tandriide281ae2016-10-12 06:02:30 -0700447 assert changelist.GetIssue(), 'CL must be uploaded first'
448 codereview_url = changelist.GetCodereviewServer()
449 assert codereview_url, 'CL must be uploaded first'
450 patchset = patchset or changelist.GetMostRecentPatchset()
451 assert patchset, 'CL must be uploaded first'
452
453 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700454 # Cache the buildbucket credentials under the codereview host key, so that
455 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700456 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000457 http = authenticator.authorize(httplib2.Http())
458 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700459
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000460 buildbucket_put_url = (
461 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000462 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000463 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700464 hostname=codereview_host,
465 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000466 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700467
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700468 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800469 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700470 if options.clobber:
471 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700472 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700473 if extra_properties:
474 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000475
476 batch_req_body = {'builds': []}
477 print_text = []
478 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700479 for bucket, builders_and_tests in sorted(buckets.iteritems()):
480 print_text.append('Bucket: %s' % bucket)
481 master = None
482 if bucket.startswith(MASTER_PREFIX):
483 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000484 for builder, tests in sorted(builders_and_tests.iteritems()):
485 print_text.append(' %s: %s' % (builder, tests))
486 parameters = {
487 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000488 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100489 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000490 'revision': options.revision,
491 }],
tandrii8c5a3532016-11-04 07:52:02 -0700492 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000493 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000494 if 'presubmit' in builder.lower():
495 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000496 if tests:
497 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700498
499 tags = [
500 'builder:%s' % builder,
501 'buildset:%s' % buildset,
502 'user_agent:git_cl_try',
503 ]
504 if master:
505 parameters['properties']['master'] = master
506 tags.append('master:%s' % master)
507
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000508 batch_req_body['builds'].append(
509 {
510 'bucket': bucket,
511 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000512 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700513 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000514 }
515 )
516
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000517 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700518 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000519 http,
520 buildbucket_put_url,
521 'PUT',
522 body=json.dumps(batch_req_body),
523 headers={'Content-Type': 'application/json'}
524 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000525 print_text.append('To see results here, run: git cl try-results')
526 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700527 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000528
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000529
tandrii221ab252016-10-06 08:12:04 -0700530def fetch_try_jobs(auth_config, changelist, buildbucket_host,
531 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700532 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000533
qyearsley53f48a12016-09-01 10:45:13 -0700534 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535 """
tandrii221ab252016-10-06 08:12:04 -0700536 assert buildbucket_host
537 assert changelist.GetIssue(), 'CL must be uploaded first'
538 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
539 patchset = patchset or changelist.GetMostRecentPatchset()
540 assert patchset, 'CL must be uploaded first'
541
542 codereview_url = changelist.GetCodereviewServer()
543 codereview_host = urlparse.urlparse(codereview_url).hostname
544 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000545 if authenticator.has_cached_credentials():
546 http = authenticator.authorize(httplib2.Http())
547 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700548 print('Warning: Some results might be missing because %s' %
549 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700550 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 http = httplib2.Http()
552
553 http.force_exception_to_status_code = True
554
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000555 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700556 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000557 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700558 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 params = {'tag': 'buildset:%s' % buildset}
560
561 builds = {}
562 while True:
563 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700564 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700566 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567 for build in content.get('builds', []):
568 builds[build['id']] = build
569 if 'next_cursor' in content:
570 params['start_cursor'] = content['next_cursor']
571 else:
572 break
573 return builds
574
575
qyearsleyeab3c042016-08-24 09:18:28 -0700576def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577 """Prints nicely result of fetch_try_jobs."""
578 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700579 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000580 return
581
582 # Make a copy, because we'll be modifying builds dictionary.
583 builds = builds.copy()
584 builder_names_cache = {}
585
586 def get_builder(b):
587 try:
588 return builder_names_cache[b['id']]
589 except KeyError:
590 try:
591 parameters = json.loads(b['parameters_json'])
592 name = parameters['builder_name']
593 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700594 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700595 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000596 name = None
597 builder_names_cache[b['id']] = name
598 return name
599
600 def get_bucket(b):
601 bucket = b['bucket']
602 if bucket.startswith('master.'):
603 return bucket[len('master.'):]
604 return bucket
605
606 if options.print_master:
607 name_fmt = '%%-%ds %%-%ds' % (
608 max(len(str(get_bucket(b))) for b in builds.itervalues()),
609 max(len(str(get_builder(b))) for b in builds.itervalues()))
610 def get_name(b):
611 return name_fmt % (get_bucket(b), get_builder(b))
612 else:
613 name_fmt = '%%-%ds' % (
614 max(len(str(get_builder(b))) for b in builds.itervalues()))
615 def get_name(b):
616 return name_fmt % get_builder(b)
617
618 def sort_key(b):
619 return b['status'], b.get('result'), get_name(b), b.get('url')
620
621 def pop(title, f, color=None, **kwargs):
622 """Pop matching builds from `builds` dict and print them."""
623
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000624 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000625 colorize = str
626 else:
627 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
628
629 result = []
630 for b in builds.values():
631 if all(b.get(k) == v for k, v in kwargs.iteritems()):
632 builds.pop(b['id'])
633 result.append(b)
634 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700635 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000636 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700637 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000638
639 total = len(builds)
640 pop(status='COMPLETED', result='SUCCESS',
641 title='Successes:', color=Fore.GREEN,
642 f=lambda b: (get_name(b), b.get('url')))
643 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
644 title='Infra Failures:', color=Fore.MAGENTA,
645 f=lambda b: (get_name(b), b.get('url')))
646 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
647 title='Failures:', color=Fore.RED,
648 f=lambda b: (get_name(b), b.get('url')))
649 pop(status='COMPLETED', result='CANCELED',
650 title='Canceled:', color=Fore.MAGENTA,
651 f=lambda b: (get_name(b),))
652 pop(status='COMPLETED', result='FAILURE',
653 failure_reason='INVALID_BUILD_DEFINITION',
654 title='Wrong master/builder name:', color=Fore.MAGENTA,
655 f=lambda b: (get_name(b),))
656 pop(status='COMPLETED', result='FAILURE',
657 title='Other failures:',
658 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
659 pop(status='COMPLETED',
660 title='Other finished:',
661 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
662 pop(status='STARTED',
663 title='Started:', color=Fore.YELLOW,
664 f=lambda b: (get_name(b), b.get('url')))
665 pop(status='SCHEDULED',
666 title='Scheduled:',
667 f=lambda b: (get_name(b), 'id=%s' % b['id']))
668 # The last section is just in case buildbucket API changes OR there is a bug.
669 pop(title='Other:',
670 f=lambda b: (get_name(b), 'id=%s' % b['id']))
671 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700672 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000673
674
Aiden Bennerc08566e2018-10-03 17:52:42 +0000675def _ComputeDiffLineRanges(files, upstream_commit):
676 """Gets the changed line ranges for each file since upstream_commit.
677
678 Parses a git diff on provided files and returns a dict that maps a file name
679 to an ordered list of range tuples in the form (start_line, count).
680 Ranges are in the same format as a git diff.
681 """
682 # If files is empty then diff_output will be a full diff.
683 if len(files) == 0:
684 return {}
685
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000686 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000687 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
688 diff_output = RunGit(diff_cmd)
689
690 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
691 # 2 capture groups
692 # 0 == fname of diff file
693 # 1 == 'diff_start,diff_count' or 'diff_start'
694 # will match each of
695 # diff --git a/foo.foo b/foo.py
696 # @@ -12,2 +14,3 @@
697 # @@ -12,2 +17 @@
698 # running re.findall on the above string with pattern will give
699 # [('foo.py', ''), ('', '14,3'), ('', '17')]
700
701 curr_file = None
702 line_diffs = {}
703 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
704 if match[0] != '':
705 # Will match the second filename in diff --git a/a.py b/b.py.
706 curr_file = match[0]
707 line_diffs[curr_file] = []
708 else:
709 # Matches +14,3
710 if ',' in match[1]:
711 diff_start, diff_count = match[1].split(',')
712 else:
713 # Single line changes are of the form +12 instead of +12,1.
714 diff_start = match[1]
715 diff_count = 1
716
717 diff_start = int(diff_start)
718 diff_count = int(diff_count)
719
720 # If diff_count == 0 this is a removal we can ignore.
721 line_diffs[curr_file].append((diff_start, diff_count))
722
723 return line_diffs
724
725
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000726def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000727 """Checks if a yapf file is in any parent directory of fpath until top_dir.
728
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000729 Recursively checks parent directories to find yapf file and if no yapf file
730 is found returns None. Uses yapf_config_cache as a cache for
731 previously found configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000732 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000733 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000734 # Return result if we've already computed it.
735 if fpath in yapf_config_cache:
736 return yapf_config_cache[fpath]
737
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000738 parent_dir = os.path.dirname(fpath)
739 if os.path.isfile(fpath):
740 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000741 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000742 # Otherwise fpath is a directory
743 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
744 if os.path.isfile(yapf_file):
745 ret = yapf_file
746 elif fpath == top_dir or parent_dir == fpath:
747 # If we're at the top level directory, or if we're at root
748 # there is no provided style.
749 ret = None
750 else:
751 # Otherwise recurse on the current directory.
752 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000753 yapf_config_cache[fpath] = ret
754 return ret
755
756
qyearsley53f48a12016-09-01 10:45:13 -0700757def write_try_results_json(output_file, builds):
758 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
759
760 The input |builds| dict is assumed to be generated by Buildbucket.
761 Buildbucket documentation: http://goo.gl/G0s101
762 """
763
764 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800765 """Extracts some of the information from one build dict."""
766 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700767 return {
768 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700769 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800770 'builder_name': parameters.get('builder_name'),
771 'created_ts': build.get('created_ts'),
772 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700773 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800774 'result': build.get('result'),
775 'status': build.get('status'),
776 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700777 'url': build.get('url'),
778 }
779
780 converted = []
781 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000782 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700783 write_json(output_file, converted)
784
785
Aaron Gable13101a62018-02-09 13:20:41 -0800786def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000787 """Prints statistics about the change to the user."""
788 # --no-ext-diff is broken in some versions of Git, so try to work around
789 # this by overriding the environment (but there is still a problem if the
790 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000791 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000792 if 'GIT_EXTERNAL_DIFF' in env:
793 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000794
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000795 try:
796 stdout = sys.stdout.fileno()
797 except AttributeError:
798 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000799 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800800 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000801 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000802
803
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000804class BuildbucketResponseException(Exception):
805 pass
806
807
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808class Settings(object):
809 def __init__(self):
810 self.default_server = None
811 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000812 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813 self.tree_status_url = None
814 self.viewvc_url = None
815 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000816 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000817 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000818 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000819 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000820 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000821 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822
823 def LazyUpdateIfNeeded(self):
824 """Updates the settings from a codereview.settings file, if available."""
825 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000826 # The only value that actually changes the behavior is
827 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000828 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000829 error_ok=True
830 ).strip().lower()
831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000833 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 LoadCodereviewSettingsFromFile(cr_settings_file)
835 self.updated = True
836
837 def GetDefaultServerUrl(self, error_ok=False):
838 if not self.default_server:
839 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000840 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000841 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 if error_ok:
843 return self.default_server
844 if not self.default_server:
845 error_message = ('Could not find settings file. You must configure '
846 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000847 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000848 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 return self.default_server
850
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000851 @staticmethod
852 def GetRelativeRoot():
853 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000854
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000856 if self.root is None:
857 self.root = os.path.abspath(self.GetRelativeRoot())
858 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000860 def GetGitMirror(self, remote='origin'):
861 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000862 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000863 if not os.path.isdir(local_url):
864 return None
865 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
866 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100867 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100868 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000869 if mirror.exists():
870 return mirror
871 return None
872
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873 def GetTreeStatusUrl(self, error_ok=False):
874 if not self.tree_status_url:
875 error_message = ('You must configure your tree status URL by running '
876 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000877 self.tree_status_url = self._GetRietveldConfig(
878 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 return self.tree_status_url
880
881 def GetViewVCUrl(self):
882 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000883 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884 return self.viewvc_url
885
rmistry@google.com90752582014-01-14 21:04:50 +0000886 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000887 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000888
rmistry@google.com78948ed2015-07-08 23:09:57 +0000889 def GetIsSkipDependencyUpload(self, branch_name):
890 """Returns true if specified branch should skip dep uploads."""
891 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
892 error_ok=True)
893
rmistry@google.com5626a922015-02-26 14:03:30 +0000894 def GetRunPostUploadHook(self):
895 run_post_upload_hook = self._GetRietveldConfig(
896 'run-post-upload-hook', error_ok=True)
897 return run_post_upload_hook == "True"
898
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000899 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000900 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000901
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000902 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000903 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000904
ukai@chromium.orge8077812012-02-03 03:41:46 +0000905 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700906 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000907 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700908 self.is_gerrit = (
909 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000910 return self.is_gerrit
911
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000912 def GetSquashGerritUploads(self):
913 """Return true if uploads to Gerrit should be squashed by default."""
914 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700915 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
916 if self.squash_gerrit_uploads is None:
917 # Default is squash now (http://crbug.com/611892#c23).
918 self.squash_gerrit_uploads = not (
919 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
920 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000921 return self.squash_gerrit_uploads
922
tandriia60502f2016-06-20 02:01:53 -0700923 def GetSquashGerritUploadsOverride(self):
924 """Return True or False if codereview.settings should be overridden.
925
926 Returns None if no override has been defined.
927 """
928 # See also http://crbug.com/611892#c23
929 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
930 error_ok=True).strip()
931 if result == 'true':
932 return True
933 if result == 'false':
934 return False
935 return None
936
tandrii@chromium.org28253532016-04-14 13:46:56 +0000937 def GetGerritSkipEnsureAuthenticated(self):
938 """Return True if EnsureAuthenticated should not be done for Gerrit
939 uploads."""
940 if self.gerrit_skip_ensure_authenticated is None:
941 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000942 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000943 error_ok=True).strip() == 'true')
944 return self.gerrit_skip_ensure_authenticated
945
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000946 def GetGitEditor(self):
947 """Return the editor specified in the git config, or None if none is."""
948 if self.git_editor is None:
949 self.git_editor = self._GetConfig('core.editor', error_ok=True)
950 return self.git_editor or None
951
thestig@chromium.org44202a22014-03-11 19:22:18 +0000952 def GetLintRegex(self):
953 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
954 DEFAULT_LINT_REGEX)
955
956 def GetLintIgnoreRegex(self):
957 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
958 DEFAULT_LINT_IGNORE_REGEX)
959
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000960 def GetProject(self):
961 if not self.project:
962 self.project = self._GetRietveldConfig('project', error_ok=True)
963 return self.project
964
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000965 def _GetRietveldConfig(self, param, **kwargs):
966 return self._GetConfig('rietveld.' + param, **kwargs)
967
rmistry@google.com78948ed2015-07-08 23:09:57 +0000968 def _GetBranchConfig(self, branch_name, param, **kwargs):
969 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
970
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000971 def _GetConfig(self, param, **kwargs):
972 self.LazyUpdateIfNeeded()
973 return RunGit(['config', param], **kwargs).strip()
974
975
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100976@contextlib.contextmanager
977def _get_gerrit_project_config_file(remote_url):
978 """Context manager to fetch and store Gerrit's project.config from
979 refs/meta/config branch and store it in temp file.
980
981 Provides a temporary filename or None if there was error.
982 """
983 error, _ = RunGitWithCode([
984 'fetch', remote_url,
985 '+refs/meta/config:refs/git_cl/meta/config'])
986 if error:
987 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700988 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100989 (remote_url, error))
990 yield None
991 return
992
993 error, project_config_data = RunGitWithCode(
994 ['show', 'refs/git_cl/meta/config:project.config'])
995 if error:
996 print('WARNING: project.config file not found')
997 yield None
998 return
999
1000 with gclient_utils.temporary_directory() as tempdir:
1001 project_config_file = os.path.join(tempdir, 'project.config')
1002 gclient_utils.FileWrite(project_config_file, project_config_data)
1003 yield project_config_file
1004
1005
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006def ShortBranchName(branch):
1007 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001008 return branch.replace('refs/heads/', '', 1)
1009
1010
1011def GetCurrentBranchRef():
1012 """Returns branch ref (e.g., refs/heads/master) or None."""
1013 return RunGit(['symbolic-ref', 'HEAD'],
1014 stderr=subprocess2.VOID, error_ok=True).strip() or None
1015
1016
1017def GetCurrentBranch():
1018 """Returns current branch or None.
1019
1020 For refs/heads/* branches, returns just last part. For others, full ref.
1021 """
1022 branchref = GetCurrentBranchRef()
1023 if branchref:
1024 return ShortBranchName(branchref)
1025 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026
1027
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001028class _CQState(object):
1029 """Enum for states of CL with respect to Commit Queue."""
1030 NONE = 'none'
1031 DRY_RUN = 'dry_run'
1032 COMMIT = 'commit'
1033
1034 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1035
1036
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001037class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001038 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001039 self.issue = issue
1040 self.patchset = patchset
1041 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001042 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001043 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001044
1045 @property
1046 def valid(self):
1047 return self.issue is not None
1048
1049
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001050def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001051 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1052 fail_result = _ParsedIssueNumberArgument()
1053
1054 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001055 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001056 if not arg.startswith('http'):
1057 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001058
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001059 url = gclient_utils.UpgradeToHttps(arg)
1060 try:
1061 parsed_url = urlparse.urlparse(url)
1062 except ValueError:
1063 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001064
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001065 if codereview is not None:
1066 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1067 return parsed or fail_result
1068
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001069 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001070
1071
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001072def _create_description_from_log(args):
1073 """Pulls out the commit log to use as a base for the CL description."""
1074 log_args = []
1075 if len(args) == 1 and not args[0].endswith('.'):
1076 log_args = [args[0] + '..']
1077 elif len(args) == 1 and args[0].endswith('...'):
1078 log_args = [args[0][:-1]]
1079 elif len(args) == 2:
1080 log_args = [args[0] + '..' + args[1]]
1081 else:
1082 log_args = args[:] # Hope for the best!
1083 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1084
1085
Aaron Gablea45ee112016-11-22 15:14:38 -08001086class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001087 def __init__(self, issue, url):
1088 self.issue = issue
1089 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001090 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001091
1092 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001093 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001094 self.issue, self.url)
1095
1096
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001097_CommentSummary = collections.namedtuple(
1098 '_CommentSummary', ['date', 'message', 'sender',
1099 # TODO(tandrii): these two aren't known in Gerrit.
1100 'approval', 'disapproval'])
1101
1102
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001104 """Changelist works with one changelist in local branch.
1105
1106 Supports two codereview backends: Rietveld or Gerrit, selected at object
1107 creation.
1108
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001109 Notes:
1110 * Not safe for concurrent multi-{thread,process} use.
1111 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001112 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001113 """
1114
1115 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1116 """Create a new ChangeList instance.
1117
1118 If issue is given, the codereview must be given too.
1119
1120 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1121 Otherwise, it's decided based on current configuration of the local branch,
1122 with default being 'rietveld' for backwards compatibility.
1123 See _load_codereview_impl for more details.
1124
1125 **kwargs will be passed directly to codereview implementation.
1126 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001128 global settings
1129 if not settings:
1130 # Happens when git_cl.py is used as a utility library.
1131 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001132
1133 if issue:
1134 assert codereview, 'codereview must be known, if issue is known'
1135
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001136 self.branchref = branchref
1137 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001138 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 self.branch = ShortBranchName(self.branchref)
1140 else:
1141 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001143 self.lookedup_issue = False
1144 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 self.has_description = False
1146 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001147 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001149 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001150 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001151 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001152 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001153
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001154 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001155 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001156 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001157 assert self._codereview_impl
1158 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001159
1160 def _load_codereview_impl(self, codereview=None, **kwargs):
1161 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001162 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1163 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1164 self._codereview = codereview
1165 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001166 return
1167
1168 # Automatic selection based on issue number set for a current branch.
1169 # Rietveld takes precedence over Gerrit.
1170 assert not self.issue
1171 # Whether we find issue or not, we are doing the lookup.
1172 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001173 if self.GetBranch():
1174 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1175 issue = _git_get_branch_config_value(
1176 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1177 if issue:
1178 self._codereview = codereview
1179 self._codereview_impl = cls(self, **kwargs)
1180 self.issue = int(issue)
1181 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001182
1183 # No issue is set for this branch, so decide based on repo-wide settings.
1184 return self._load_codereview_impl(
1185 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1186 **kwargs)
1187
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001188 def IsGerrit(self):
1189 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001190
1191 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001192 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001193
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001194 The return value is a string suitable for passing to git cl with the --cc
1195 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001196 """
1197 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001198 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001199 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001200 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1201 return self.cc
1202
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001203 def GetCCListWithoutDefault(self):
1204 """Return the users cc'd on this CL excluding default ones."""
1205 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001206 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001207 return self.cc
1208
Daniel Cheng7227d212017-11-17 08:12:37 -08001209 def ExtendCC(self, more_cc):
1210 """Extends the list of users to cc on this CL based on the changed files."""
1211 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212
1213 def GetBranch(self):
1214 """Returns the short branch name, e.g. 'master'."""
1215 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001216 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001217 if not branchref:
1218 return None
1219 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 self.branch = ShortBranchName(self.branchref)
1221 return self.branch
1222
1223 def GetBranchRef(self):
1224 """Returns the full branch name, e.g. 'refs/heads/master'."""
1225 self.GetBranch() # Poke the lazy loader.
1226 return self.branchref
1227
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001228 def ClearBranch(self):
1229 """Clears cached branch data of this object."""
1230 self.branch = self.branchref = None
1231
tandrii5d48c322016-08-18 16:19:37 -07001232 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1233 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1234 kwargs['branch'] = self.GetBranch()
1235 return _git_get_branch_config_value(key, default, **kwargs)
1236
1237 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1238 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1239 assert self.GetBranch(), (
1240 'this CL must have an associated branch to %sset %s%s' %
1241 ('un' if value is None else '',
1242 key,
1243 '' if value is None else ' to %r' % value))
1244 kwargs['branch'] = self.GetBranch()
1245 return _git_set_branch_config_value(key, value, **kwargs)
1246
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001247 @staticmethod
1248 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001249 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 e.g. 'origin', 'refs/heads/master'
1251 """
1252 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001253 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1254
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001256 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001258 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1259 error_ok=True).strip()
1260 if upstream_branch:
1261 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001263 # Else, try to guess the origin remote.
1264 remote_branches = RunGit(['branch', '-r']).split()
1265 if 'origin/master' in remote_branches:
1266 # Fall back on origin/master if it exits.
1267 remote = 'origin'
1268 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001270 DieWithError(
1271 'Unable to determine default branch to diff against.\n'
1272 'Either pass complete "git diff"-style arguments, like\n'
1273 ' git cl upload origin/master\n'
1274 'or verify this branch is set up to track another \n'
1275 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276
1277 return remote, upstream_branch
1278
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001279 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001280 upstream_branch = self.GetUpstreamBranch()
1281 if not BranchExists(upstream_branch):
1282 DieWithError('The upstream for the current branch (%s) does not exist '
1283 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001284 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001285 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001286
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 def GetUpstreamBranch(self):
1288 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001289 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001291 upstream_branch = upstream_branch.replace('refs/heads/',
1292 'refs/remotes/%s/' % remote)
1293 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1294 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295 self.upstream_branch = upstream_branch
1296 return self.upstream_branch
1297
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001298 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001299 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 remote, branch = None, self.GetBranch()
1301 seen_branches = set()
1302 while branch not in seen_branches:
1303 seen_branches.add(branch)
1304 remote, branch = self.FetchUpstreamTuple(branch)
1305 branch = ShortBranchName(branch)
1306 if remote != '.' or branch.startswith('refs/remotes'):
1307 break
1308 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001309 remotes = RunGit(['remote'], error_ok=True).split()
1310 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001311 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001312 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001313 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001314 logging.warn('Could not determine which remote this change is '
1315 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001316 else:
1317 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001318 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001319 branch = 'HEAD'
1320 if branch.startswith('refs/remotes'):
1321 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001322 elif branch.startswith('refs/branch-heads/'):
1323 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001324 else:
1325 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001326 return self._remote
1327
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001328 def GitSanityChecks(self, upstream_git_obj):
1329 """Checks git repo status and ensures diff is from local commits."""
1330
sbc@chromium.org79706062015-01-14 21:18:12 +00001331 if upstream_git_obj is None:
1332 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001333 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001334 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001335 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001336 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001337 return False
1338
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001339 # Verify the commit we're diffing against is in our current branch.
1340 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1341 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1342 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001343 print('ERROR: %s is not in the current branch. You may need to rebase '
1344 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001345 return False
1346
1347 # List the commits inside the diff, and verify they are all local.
1348 commits_in_diff = RunGit(
1349 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1350 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1351 remote_branch = remote_branch.strip()
1352 if code != 0:
1353 _, remote_branch = self.GetRemoteBranch()
1354
1355 commits_in_remote = RunGit(
1356 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1357
1358 common_commits = set(commits_in_diff) & set(commits_in_remote)
1359 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001360 print('ERROR: Your diff contains %d commits already in %s.\n'
1361 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1362 'the diff. If you are using a custom git flow, you can override'
1363 ' the reference used for this check with "git config '
1364 'gitcl.remotebranch <git-ref>".' % (
1365 len(common_commits), remote_branch, upstream_git_obj),
1366 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001367 return False
1368 return True
1369
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001370 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001371 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001372
1373 Returns None if it is not set.
1374 """
tandrii5d48c322016-08-18 16:19:37 -07001375 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 def GetRemoteUrl(self):
1378 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1379
1380 Returns None if there is no remote.
1381 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001382 is_cached, value = self._cached_remote_url
1383 if is_cached:
1384 return value
1385
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001386 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001387 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1388
1389 # If URL is pointing to a local directory, it is probably a git cache.
1390 if os.path.isdir(url):
1391 url = RunGit(['config', 'remote.%s.url' % remote],
1392 error_ok=True,
1393 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001394 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001395 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001397 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001398 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001399 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001400 self.issue = self._GitGetBranchConfigValue(
1401 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001402 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 return self.issue
1404
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 def GetIssueURL(self):
1406 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001407 issue = self.GetIssue()
1408 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001409 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001410 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001412 def GetDescription(self, pretty=False, force=False):
1413 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001415 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 self.has_description = True
1417 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001418 # Set width to 72 columns + 2 space indent.
1419 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001421 lines = self.description.splitlines()
1422 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423 return self.description
1424
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001425 def GetDescriptionFooters(self):
1426 """Returns (non_footer_lines, footers) for the commit message.
1427
1428 Returns:
1429 non_footer_lines (list(str)) - Simple list of description lines without
1430 any footer. The lines do not contain newlines, nor does the list contain
1431 the empty line between the message and the footers.
1432 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1433 [("Change-Id", "Ideadbeef...."), ...]
1434 """
1435 raw_description = self.GetDescription()
1436 msg_lines, _, footers = git_footers.split_footers(raw_description)
1437 if footers:
1438 msg_lines = msg_lines[:len(msg_lines)-1]
1439 return msg_lines, footers
1440
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001442 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001443 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001444 self.patchset = self._GitGetBranchConfigValue(
1445 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001446 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447 return self.patchset
1448
1449 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001450 """Set this branch's patchset. If patchset=0, clears the patchset."""
1451 assert self.GetBranch()
1452 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001453 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001454 else:
1455 self.patchset = int(patchset)
1456 self._GitSetBranchConfigValue(
1457 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001459 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001460 """Set this branch's issue. If issue isn't given, clears the issue."""
1461 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001463 issue = int(issue)
1464 self._GitSetBranchConfigValue(
1465 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001466 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001467 codereview_server = self._codereview_impl.GetCodereviewServer()
1468 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001469 self._GitSetBranchConfigValue(
1470 self._codereview_impl.CodereviewServerConfigKey(),
1471 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472 else:
tandrii5d48c322016-08-18 16:19:37 -07001473 # Reset all of these just to be clean.
1474 reset_suffixes = [
1475 'last-upload-hash',
1476 self._codereview_impl.IssueConfigKey(),
1477 self._codereview_impl.PatchsetConfigKey(),
1478 self._codereview_impl.CodereviewServerConfigKey(),
1479 ] + self._PostUnsetIssueProperties()
1480 for prop in reset_suffixes:
1481 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001482 msg = RunGit(['log', '-1', '--format=%B']).strip()
1483 if msg and git_footers.get_footer_change_id(msg):
1484 print('WARNING: The change patched into this branch has a Change-Id. '
1485 'Removing it.')
1486 RunGit(['commit', '--amend', '-m',
1487 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001488 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001489 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490
dnjba1b0f32016-09-02 12:37:42 -07001491 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001492 if not self.GitSanityChecks(upstream_branch):
1493 DieWithError('\nGit sanity check failure')
1494
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001495 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001496 if not root:
1497 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001498 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001499
1500 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001501 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001502 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001503 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001504 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001505 except subprocess2.CalledProcessError:
1506 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001507 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001508 'This branch probably doesn\'t exist anymore. To reset the\n'
1509 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001510 ' git branch --set-upstream-to origin/master %s\n'
1511 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001512 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001513
maruel@chromium.org52424302012-08-29 15:14:30 +00001514 issue = self.GetIssue()
1515 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001516 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001517 description = self.GetDescription()
1518 else:
1519 # If the change was never uploaded, use the log messages of all commits
1520 # up to the branch point, as git cl upload will prefill the description
1521 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001522 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1523 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001524
1525 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001526 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001527 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001528 name,
1529 description,
1530 absroot,
1531 files,
1532 issue,
1533 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001534 author,
1535 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001536
dsansomee2d6fd92016-09-08 00:10:47 -07001537 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001538 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001539 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001540 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001541
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001542 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1543 """Sets the description for this CL remotely.
1544
1545 You can get description_lines and footers with GetDescriptionFooters.
1546
1547 Args:
1548 description_lines (list(str)) - List of CL description lines without
1549 newline characters.
1550 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1551 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1552 `List-Of-Tokens`). It will be case-normalized so that each token is
1553 title-cased.
1554 """
1555 new_description = '\n'.join(description_lines)
1556 if footers:
1557 new_description += '\n'
1558 for k, v in footers:
1559 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1560 if not git_footers.FOOTER_PATTERN.match(foot):
1561 raise ValueError('Invalid footer %r' % foot)
1562 new_description += foot + '\n'
1563 self.UpdateDescription(new_description, force)
1564
Edward Lesmes8e282792018-04-03 18:50:29 -04001565 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001566 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1567 try:
1568 return presubmit_support.DoPresubmitChecks(change, committing,
1569 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1570 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001571 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1572 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001573 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001574 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001575
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001576 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1577 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001578 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1579 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001580 else:
1581 # Assume url.
1582 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1583 urlparse.urlparse(issue_arg))
1584 if not parsed_issue_arg or not parsed_issue_arg.valid:
1585 DieWithError('Failed to parse issue argument "%s". '
1586 'Must be an issue number or a valid URL.' % issue_arg)
1587 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001588 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001589
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001590 def CMDUpload(self, options, git_diff_args, orig_args):
1591 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001592 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001593 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001594 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001595 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001596 else:
1597 if self.GetBranch() is None:
1598 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1599
1600 # Default to diffing against common ancestor of upstream branch
1601 base_branch = self.GetCommonAncestorWithUpstream()
1602 git_diff_args = [base_branch, 'HEAD']
1603
Aaron Gablec4c40d12017-05-22 11:49:53 -07001604
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001605 # Fast best-effort checks to abort before running potentially
1606 # expensive hooks if uploading is likely to fail anyway. Passing these
1607 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001608 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001609 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001610
1611 # Apply watchlists on upload.
1612 change = self.GetChange(base_branch, None)
1613 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1614 files = [f.LocalPath() for f in change.AffectedFiles()]
1615 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001616 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001617
1618 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001619 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001620 # Set the reviewer list now so that presubmit checks can access it.
1621 change_description = ChangeDescription(change.FullDescriptionText())
1622 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001623 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001624 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001625 change)
1626 change.SetDescriptionText(change_description.description)
1627 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001628 may_prompt=not options.force,
1629 verbose=options.verbose,
1630 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001631 if not hook_results.should_continue():
1632 return 1
1633 if not options.reviewers and hook_results.reviewers:
1634 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001635 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001636
Aaron Gable13101a62018-02-09 13:20:41 -08001637 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001638 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001639 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001640 _git_set_branch_config_value('last-upload-hash',
1641 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001642 # Run post upload hooks, if specified.
1643 if settings.GetRunPostUploadHook():
1644 presubmit_support.DoPostUploadExecuter(
1645 change,
1646 self,
1647 settings.GetRoot(),
1648 options.verbose,
1649 sys.stdout)
1650
1651 # Upload all dependencies if specified.
1652 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001653 print()
1654 print('--dependencies has been specified.')
1655 print('All dependent local branches will be re-uploaded.')
1656 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001657 # Remove the dependencies flag from args so that we do not end up in a
1658 # loop.
1659 orig_args.remove('--dependencies')
1660 ret = upload_branch_deps(self, orig_args)
1661 return ret
1662
Ravi Mistry31e7d562018-04-02 12:53:57 -04001663 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1664 """Sets labels on the change based on the provided flags.
1665
1666 Sets labels if issue is already uploaded and known, else returns without
1667 doing anything.
1668
1669 Args:
1670 enable_auto_submit: Sets Auto-Submit+1 on the change.
1671 use_commit_queue: Sets Commit-Queue+2 on the change.
1672 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1673 both use_commit_queue and cq_dry_run are true.
1674 """
1675 if not self.GetIssue():
1676 return
1677 try:
1678 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1679 cq_dry_run)
1680 return 0
1681 except KeyboardInterrupt:
1682 raise
1683 except:
1684 labels = []
1685 if enable_auto_submit:
1686 labels.append('Auto-Submit')
1687 if use_commit_queue or cq_dry_run:
1688 labels.append('Commit-Queue')
1689 print('WARNING: Failed to set label(s) on your change: %s\n'
1690 'Either:\n'
1691 ' * Your project does not have the above label(s),\n'
1692 ' * You don\'t have permission to set the above label(s),\n'
1693 ' * There\'s a bug in this code (see stack trace below).\n' %
1694 (', '.join(labels)))
1695 # Still raise exception so that stack trace is printed.
1696 raise
1697
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001698 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001699 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001700
1701 Issue must have been already uploaded and known.
1702 """
1703 assert new_state in _CQState.ALL_STATES
1704 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001705 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001706 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001707 return 0
1708 except KeyboardInterrupt:
1709 raise
1710 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001711 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001712 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001713 ' * Your project has no CQ,\n'
1714 ' * You don\'t have permission to change the CQ state,\n'
1715 ' * There\'s a bug in this code (see stack trace below).\n'
1716 'Consider specifying which bots to trigger manually or asking your '
1717 'project owners for permissions or contacting Chrome Infra at:\n'
1718 'https://www.chromium.org/infra\n\n' %
1719 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001720 # Still raise exception so that stack trace is printed.
1721 raise
1722
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001723 # Forward methods to codereview specific implementation.
1724
Aaron Gable636b13f2017-07-14 10:42:48 -07001725 def AddComment(self, message, publish=None):
1726 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001727
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001728 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001729 """Returns list of _CommentSummary for each comment.
1730
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001731 args:
1732 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001733 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001734 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001735
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001736 def CloseIssue(self):
1737 return self._codereview_impl.CloseIssue()
1738
1739 def GetStatus(self):
1740 return self._codereview_impl.GetStatus()
1741
1742 def GetCodereviewServer(self):
1743 return self._codereview_impl.GetCodereviewServer()
1744
tandriide281ae2016-10-12 06:02:30 -07001745 def GetIssueOwner(self):
1746 """Get owner from codereview, which may differ from this checkout."""
1747 return self._codereview_impl.GetIssueOwner()
1748
Edward Lemur707d70b2018-02-07 00:50:14 +01001749 def GetReviewers(self):
1750 return self._codereview_impl.GetReviewers()
1751
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001752 def GetMostRecentPatchset(self):
1753 return self._codereview_impl.GetMostRecentPatchset()
1754
tandriide281ae2016-10-12 06:02:30 -07001755 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001756 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001757 return self._codereview_impl.CannotTriggerTryJobReason()
1758
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001759 def GetTryJobProperties(self, patchset=None):
1760 """Returns dictionary of properties to launch try job."""
1761 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001762
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001763 def __getattr__(self, attr):
1764 # This is because lots of untested code accesses Rietveld-specific stuff
1765 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001766 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001767 # Note that child method defines __getattr__ as well, and forwards it here,
1768 # because _RietveldChangelistImpl is not cleaned up yet, and given
1769 # deprecation of Rietveld, it should probably be just removed.
1770 # Until that time, avoid infinite recursion by bypassing __getattr__
1771 # of implementation class.
1772 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001773
1774
1775class _ChangelistCodereviewBase(object):
1776 """Abstract base class encapsulating codereview specifics of a changelist."""
1777 def __init__(self, changelist):
1778 self._changelist = changelist # instance of Changelist
1779
1780 def __getattr__(self, attr):
1781 # Forward methods to changelist.
1782 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1783 # _RietveldChangelistImpl to avoid this hack?
1784 return getattr(self._changelist, attr)
1785
1786 def GetStatus(self):
1787 """Apply a rough heuristic to give a simple summary of an issue's review
1788 or CQ status, assuming adherence to a common workflow.
1789
1790 Returns None if no issue for this branch, or specific string keywords.
1791 """
1792 raise NotImplementedError()
1793
1794 def GetCodereviewServer(self):
1795 """Returns server URL without end slash, like "https://codereview.com"."""
1796 raise NotImplementedError()
1797
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001798 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001799 """Fetches and returns description from the codereview server."""
1800 raise NotImplementedError()
1801
tandrii5d48c322016-08-18 16:19:37 -07001802 @classmethod
1803 def IssueConfigKey(cls):
1804 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001805 raise NotImplementedError()
1806
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001807 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001808 def PatchsetConfigKey(cls):
1809 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001810 raise NotImplementedError()
1811
tandrii5d48c322016-08-18 16:19:37 -07001812 @classmethod
1813 def CodereviewServerConfigKey(cls):
1814 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001815 raise NotImplementedError()
1816
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001817 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001818 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001819 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001820
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001821 def GetGerritObjForPresubmit(self):
1822 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1823 return None
1824
dsansomee2d6fd92016-09-08 00:10:47 -07001825 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001826 """Update the description on codereview site."""
1827 raise NotImplementedError()
1828
Aaron Gable636b13f2017-07-14 10:42:48 -07001829 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001830 """Posts a comment to the codereview site."""
1831 raise NotImplementedError()
1832
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001833 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001834 raise NotImplementedError()
1835
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001836 def CloseIssue(self):
1837 """Closes the issue."""
1838 raise NotImplementedError()
1839
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001840 def GetMostRecentPatchset(self):
1841 """Returns the most recent patchset number from the codereview site."""
1842 raise NotImplementedError()
1843
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001844 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001845 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001846 """Fetches and applies the issue.
1847
1848 Arguments:
1849 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1850 reject: if True, reject the failed patch instead of switching to 3-way
1851 merge. Rietveld only.
1852 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1853 only.
1854 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001855 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001856 """
1857 raise NotImplementedError()
1858
1859 @staticmethod
1860 def ParseIssueURL(parsed_url):
1861 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1862 failed."""
1863 raise NotImplementedError()
1864
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001865 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001866 """Best effort check that user is authenticated with codereview server.
1867
1868 Arguments:
1869 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001870 refresh: whether to attempt to refresh credentials. Ignored if not
1871 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001872 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001873 raise NotImplementedError()
1874
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001875 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001876 """Best effort check that uploading isn't supposed to fail for predictable
1877 reasons.
1878
1879 This method should raise informative exception if uploading shouldn't
1880 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001881
1882 Arguments:
1883 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001884 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001885 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001886
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001887 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001888 """Uploads a change to codereview."""
1889 raise NotImplementedError()
1890
Ravi Mistry31e7d562018-04-02 12:53:57 -04001891 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1892 """Sets labels on the change based on the provided flags.
1893
1894 Issue must have been already uploaded and known.
1895 """
1896 raise NotImplementedError()
1897
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001898 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001899 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001900
1901 Issue must have been already uploaded and known.
1902 """
1903 raise NotImplementedError()
1904
tandriie113dfd2016-10-11 10:20:12 -07001905 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001906 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001907 raise NotImplementedError()
1908
tandriide281ae2016-10-12 06:02:30 -07001909 def GetIssueOwner(self):
1910 raise NotImplementedError()
1911
Edward Lemur707d70b2018-02-07 00:50:14 +01001912 def GetReviewers(self):
1913 raise NotImplementedError()
1914
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001915 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001916 raise NotImplementedError()
1917
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001918
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001919class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001920 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001921 # auth_config is Rietveld thing, kept here to preserve interface only.
1922 super(_GerritChangelistImpl, self).__init__(changelist)
1923 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001924 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001925 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001926 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001927 # Map from change number (issue) to its detail cache.
1928 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001929
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001930 if codereview_host is not None:
1931 assert not codereview_host.startswith('https://'), codereview_host
1932 self._gerrit_host = codereview_host
1933 self._gerrit_server = 'https://%s' % codereview_host
1934
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001935 def _GetGerritHost(self):
1936 # Lazy load of configs.
1937 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001938 if self._gerrit_host and '.' not in self._gerrit_host:
1939 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1940 # This happens for internal stuff http://crbug.com/614312.
1941 parsed = urlparse.urlparse(self.GetRemoteUrl())
1942 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001943 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07001944 ' Your current remote is: %s' % self.GetRemoteUrl())
1945 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1946 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001947 return self._gerrit_host
1948
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001949 def _GetGitHost(self):
1950 """Returns git host to be used when uploading change to Gerrit."""
1951 return urlparse.urlparse(self.GetRemoteUrl()).netloc
1952
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001953 def GetCodereviewServer(self):
1954 if not self._gerrit_server:
1955 # If we're on a branch then get the server potentially associated
1956 # with that branch.
1957 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001958 self._gerrit_server = self._GitGetBranchConfigValue(
1959 self.CodereviewServerConfigKey())
1960 if self._gerrit_server:
1961 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001962 if not self._gerrit_server:
1963 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1964 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001965 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001966 parts[0] = parts[0] + '-review'
1967 self._gerrit_host = '.'.join(parts)
1968 self._gerrit_server = 'https://%s' % self._gerrit_host
1969 return self._gerrit_server
1970
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001971 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001972 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001973 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001974 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001975 logging.warn('can\'t detect Gerrit project.')
1976 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001977 project = urlparse.urlparse(remote_url).path.strip('/')
1978 if project.endswith('.git'):
1979 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001980 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1981 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1982 # gitiles/git-over-https protocol. E.g.,
1983 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1984 # as
1985 # https://chromium.googlesource.com/v8/v8
1986 if project.startswith('a/'):
1987 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001988 return project
1989
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001990 def _GerritChangeIdentifier(self):
1991 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1992
1993 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001994 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001995 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001996 project = self._GetGerritProject()
1997 if project:
1998 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1999 # Fall back on still unique, but less efficient change number.
2000 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002001
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002002 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002003 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002004 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002005
tandrii5d48c322016-08-18 16:19:37 -07002006 @classmethod
2007 def PatchsetConfigKey(cls):
2008 return 'gerritpatchset'
2009
2010 @classmethod
2011 def CodereviewServerConfigKey(cls):
2012 return 'gerritserver'
2013
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002014 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002015 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002016 if settings.GetGerritSkipEnsureAuthenticated():
2017 # For projects with unusual authentication schemes.
2018 # See http://crbug.com/603378.
2019 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002020
2021 # Check presence of cookies only if using cookies-based auth method.
2022 cookie_auth = gerrit_util.Authenticator.get()
2023 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002024 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002025
2026 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002027 self.GetCodereviewServer()
2028 git_host = self._GetGitHost()
2029 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002030
2031 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2032 git_auth = cookie_auth.get_auth_header(git_host)
2033 if gerrit_auth and git_auth:
2034 if gerrit_auth == git_auth:
2035 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002036 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002037 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002038 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002039 ' %s\n'
2040 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002041 ' Consider running the following command:\n'
2042 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002043 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002044 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002045 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002046 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002047 cookie_auth.get_new_password_message(git_host)))
2048 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002049 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002050 return
2051 else:
2052 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002053 ([] if gerrit_auth else [self._gerrit_host]) +
2054 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002055 DieWithError('Credentials for the following hosts are required:\n'
2056 ' %s\n'
2057 'These are read from %s (or legacy %s)\n'
2058 '%s' % (
2059 '\n '.join(missing),
2060 cookie_auth.get_gitcookies_path(),
2061 cookie_auth.get_netrc_path(),
2062 cookie_auth.get_new_password_message(git_host)))
2063
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002064 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002065 if not self.GetIssue():
2066 return
2067
2068 # Warm change details cache now to avoid RPCs later, reducing latency for
2069 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002070 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002071 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002072
2073 status = self._GetChangeDetail()['status']
2074 if status in ('MERGED', 'ABANDONED'):
2075 DieWithError('Change %s has been %s, new uploads are not allowed' %
2076 (self.GetIssueURL(),
2077 'submitted' if status == 'MERGED' else 'abandoned'))
2078
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002079 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2080 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2081 # Apparently this check is not very important? Otherwise get_auth_email
2082 # could have been added to other implementations of Authenticator.
2083 cookies_auth = gerrit_util.Authenticator.get()
2084 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002085 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002086
2087 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002088 if self.GetIssueOwner() == cookies_user:
2089 return
2090 logging.debug('change %s owner is %s, cookies user is %s',
2091 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002092 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002093 # so ask what Gerrit thinks of this user.
2094 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2095 if details['email'] == self.GetIssueOwner():
2096 return
2097 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002098 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002099 'as %s.\n'
2100 'Uploading may fail due to lack of permissions.' %
2101 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2102 confirm_or_exit(action='upload')
2103
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002104 def _PostUnsetIssueProperties(self):
2105 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002106 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002107
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002108 def GetGerritObjForPresubmit(self):
2109 return presubmit_support.GerritAccessor(self._GetGerritHost())
2110
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002111 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002112 """Apply a rough heuristic to give a simple summary of an issue's review
2113 or CQ status, assuming adherence to a common workflow.
2114
2115 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002116 * 'error' - error from review tool (including deleted issues)
2117 * 'unsent' - no reviewers added
2118 * 'waiting' - waiting for review
2119 * 'reply' - waiting for uploader to reply to review
2120 * 'lgtm' - Code-Review label has been set
2121 * 'commit' - in the commit queue
2122 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002123 """
2124 if not self.GetIssue():
2125 return None
2126
2127 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002128 data = self._GetChangeDetail([
2129 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002130 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002131 return 'error'
2132
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002133 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002134 return 'closed'
2135
Aaron Gable9ab38c62017-04-06 14:36:33 -07002136 if data['labels'].get('Commit-Queue', {}).get('approved'):
2137 # The section will have an "approved" subsection if anyone has voted
2138 # the maximum value on the label.
2139 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002140
Aaron Gable9ab38c62017-04-06 14:36:33 -07002141 if data['labels'].get('Code-Review', {}).get('approved'):
2142 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002143
2144 if not data.get('reviewers', {}).get('REVIEWER', []):
2145 return 'unsent'
2146
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002147 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002148 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2149 last_message_author = messages.pop().get('author', {})
2150 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002151 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2152 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002153 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002154 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002155 if last_message_author.get('_account_id') == owner:
2156 # Most recent message was by owner.
2157 return 'waiting'
2158 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002159 # Some reply from non-owner.
2160 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002161
2162 # Somehow there are no messages even though there are reviewers.
2163 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002164
2165 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002166 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002167 patchset = data['revisions'][data['current_revision']]['_number']
2168 self.SetPatchset(patchset)
2169 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002170
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002171 def FetchDescription(self, force=False):
2172 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2173 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002174 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002175 return data['revisions'][current_rev]['commit']['message'].encode(
2176 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002177
dsansomee2d6fd92016-09-08 00:10:47 -07002178 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002179 if gerrit_util.HasPendingChangeEdit(
2180 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002181 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002182 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002183 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002184 'unpublished edit. Either publish the edit in the Gerrit web UI '
2185 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002186
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002187 gerrit_util.DeletePendingChangeEdit(
2188 self._GetGerritHost(), self._GerritChangeIdentifier())
2189 gerrit_util.SetCommitMessage(
2190 self._GetGerritHost(), self._GerritChangeIdentifier(),
2191 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002192
Aaron Gable636b13f2017-07-14 10:42:48 -07002193 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002194 gerrit_util.SetReview(
2195 self._GetGerritHost(), self._GerritChangeIdentifier(),
2196 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002197
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002198 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002199 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002200 messages = self._GetChangeDetail(
2201 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2202 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002203 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002204
2205 # Build dictionary of file comments for easy access and sorting later.
2206 # {author+date: {path: {patchset: {line: url+message}}}}
2207 comments = collections.defaultdict(
2208 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2209 for path, line_comments in file_comments.iteritems():
2210 for comment in line_comments:
2211 if comment.get('tag', '').startswith('autogenerated'):
2212 continue
2213 key = (comment['author']['email'], comment['updated'])
2214 if comment.get('side', 'REVISION') == 'PARENT':
2215 patchset = 'Base'
2216 else:
2217 patchset = 'PS%d' % comment['patch_set']
2218 line = comment.get('line', 0)
2219 url = ('https://%s/c/%s/%s/%s#%s%s' %
2220 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2221 'b' if comment.get('side') == 'PARENT' else '',
2222 str(line) if line else ''))
2223 comments[key][path][patchset][line] = (url, comment['message'])
2224
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002225 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002226 for msg in messages:
2227 # Don't bother showing autogenerated messages.
2228 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2229 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002230 # Gerrit spits out nanoseconds.
2231 assert len(msg['date'].split('.')[-1]) == 9
2232 date = datetime.datetime.strptime(msg['date'][:-3],
2233 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002234 message = msg['message']
2235 key = (msg['author']['email'], msg['date'])
2236 if key in comments:
2237 message += '\n'
2238 for path, patchsets in sorted(comments.get(key, {}).items()):
2239 if readable:
2240 message += '\n%s' % path
2241 for patchset, lines in sorted(patchsets.items()):
2242 for line, (url, content) in sorted(lines.items()):
2243 if line:
2244 line_str = 'Line %d' % line
2245 path_str = '%s:%d:' % (path, line)
2246 else:
2247 line_str = 'File comment'
2248 path_str = '%s:0:' % path
2249 if readable:
2250 message += '\n %s, %s: %s' % (patchset, line_str, url)
2251 message += '\n %s\n' % content
2252 else:
2253 message += '\n%s ' % path_str
2254 message += '\n%s\n' % content
2255
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002256 summary.append(_CommentSummary(
2257 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002258 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002259 sender=msg['author']['email'],
2260 # These could be inferred from the text messages and correlated with
2261 # Code-Review label maximum, however this is not reliable.
2262 # Leaving as is until the need arises.
2263 approval=False,
2264 disapproval=False,
2265 ))
2266 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002267
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002268 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002269 gerrit_util.AbandonChange(
2270 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002271
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002272 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002273 gerrit_util.SubmitChange(
2274 self._GetGerritHost(), self._GerritChangeIdentifier(),
2275 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002276
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002277 def _GetChangeDetail(self, options=None, no_cache=False):
2278 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002279
2280 If fresh data is needed, set no_cache=True which will clear cache and
2281 thus new data will be fetched from Gerrit.
2282 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002283 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002284 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002285
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002286 # Optimization to avoid multiple RPCs:
2287 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2288 'CURRENT_COMMIT' not in options):
2289 options.append('CURRENT_COMMIT')
2290
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002291 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002292 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002293 options = [o.upper() for o in options]
2294
2295 # Check in cache first unless no_cache is True.
2296 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002297 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002298 else:
2299 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002300 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002301 # Assumption: data fetched before with extra options is suitable
2302 # for return for a smaller set of options.
2303 # For example, if we cached data for
2304 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2305 # and request is for options=[CURRENT_REVISION],
2306 # THEN we can return prior cached data.
2307 if options_set.issubset(cached_options_set):
2308 return data
2309
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002310 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002311 data = gerrit_util.GetChangeDetail(
2312 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002313 except gerrit_util.GerritError as e:
2314 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002315 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002316 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002317
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002318 self._detail_cache.setdefault(cache_key, []).append(
2319 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002320 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002321
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002322 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002323 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002324 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002325 data = gerrit_util.GetChangeCommit(
2326 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002327 except gerrit_util.GerritError as e:
2328 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002329 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002330 raise
agable32978d92016-11-01 12:55:02 -07002331 return data
2332
Olivier Robin75ee7252018-04-13 10:02:56 +02002333 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002334 if git_common.is_dirty_git_tree('land'):
2335 return 1
tandriid60367b2016-06-22 05:25:12 -07002336 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2337 if u'Commit-Queue' in detail.get('labels', {}):
2338 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002339 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2340 'which can test and land changes for you. '
2341 'Are you sure you wish to bypass it?\n',
2342 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002343
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002344 differs = True
tandriic4344b52016-08-29 06:04:54 -07002345 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002346 # Note: git diff outputs nothing if there is no diff.
2347 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002348 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002349 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002350 if detail['current_revision'] == last_upload:
2351 differs = False
2352 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002353 print('WARNING: Local branch contents differ from latest uploaded '
2354 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002355 if differs:
2356 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002357 confirm_or_exit(
2358 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2359 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002360 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002361 elif not bypass_hooks:
2362 hook_results = self.RunHook(
2363 committing=True,
2364 may_prompt=not force,
2365 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002366 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2367 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002368 if not hook_results.should_continue():
2369 return 1
2370
2371 self.SubmitIssue(wait_for_merge=True)
2372 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002373 links = self._GetChangeCommit().get('web_links', [])
2374 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002375 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002376 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002377 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002378 return 0
2379
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002380 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002381 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002382 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002383 assert not directory
2384 assert parsed_issue_arg.valid
2385
2386 self._changelist.issue = parsed_issue_arg.issue
2387
2388 if parsed_issue_arg.hostname:
2389 self._gerrit_host = parsed_issue_arg.hostname
2390 self._gerrit_server = 'https://%s' % self._gerrit_host
2391
tandriic2405f52016-10-10 08:13:15 -07002392 try:
2393 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002394 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002395 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002396
2397 if not parsed_issue_arg.patchset:
2398 # Use current revision by default.
2399 revision_info = detail['revisions'][detail['current_revision']]
2400 patchset = int(revision_info['_number'])
2401 else:
2402 patchset = parsed_issue_arg.patchset
2403 for revision_info in detail['revisions'].itervalues():
2404 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2405 break
2406 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002407 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002408 (parsed_issue_arg.patchset, self.GetIssue()))
2409
Aaron Gable697a91b2018-01-19 15:20:15 -08002410 remote_url = self._changelist.GetRemoteUrl()
2411 if remote_url.endswith('.git'):
2412 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002413 remote_url = remote_url.rstrip('/')
2414
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002415 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002416 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002417
2418 if remote_url != fetch_info['url']:
2419 DieWithError('Trying to patch a change from %s but this repo appears '
2420 'to be %s.' % (fetch_info['url'], remote_url))
2421
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002422 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002423
Aaron Gable62619a32017-06-16 08:22:09 -07002424 if force:
2425 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2426 print('Checked out commit for change %i patchset %i locally' %
2427 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002428 elif nocommit:
2429 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2430 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002431 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002432 RunGit(['cherry-pick', 'FETCH_HEAD'])
2433 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002434 (parsed_issue_arg.issue, patchset))
2435 print('Note: this created a local commit which does not have '
2436 'the same hash as the one uploaded for review. This will make '
2437 'uploading changes based on top of this branch difficult.\n'
2438 'If you want to do that, use "git cl patch --force" instead.')
2439
Stefan Zagerd08043c2017-10-12 12:07:02 -07002440 if self.GetBranch():
2441 self.SetIssue(parsed_issue_arg.issue)
2442 self.SetPatchset(patchset)
2443 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2444 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2445 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2446 else:
2447 print('WARNING: You are in detached HEAD state.\n'
2448 'The patch has been applied to your checkout, but you will not be '
2449 'able to upload a new patch set to the gerrit issue.\n'
2450 'Try using the \'-b\' option if you would like to work on a '
2451 'branch and/or upload a new patch set.')
2452
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002453 return 0
2454
2455 @staticmethod
2456 def ParseIssueURL(parsed_url):
2457 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2458 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002459 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2460 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002461 # Short urls like https://domain/<issue_number> can be used, but don't allow
2462 # specifying the patchset (you'd 404), but we allow that here.
2463 if parsed_url.path == '/':
2464 part = parsed_url.fragment
2465 else:
2466 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002467 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002468 if match:
2469 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002470 issue=int(match.group(3)),
2471 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002472 hostname=parsed_url.netloc,
2473 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002474 return None
2475
tandrii16e0b4e2016-06-07 10:34:28 -07002476 def _GerritCommitMsgHookCheck(self, offer_removal):
2477 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2478 if not os.path.exists(hook):
2479 return
2480 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2481 # custom developer made one.
2482 data = gclient_utils.FileRead(hook)
2483 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2484 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002485 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002486 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002487 'and may interfere with it in subtle ways.\n'
2488 'We recommend you remove the commit-msg hook.')
2489 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002490 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002491 gclient_utils.rm_file_or_tree(hook)
2492 print('Gerrit commit-msg hook removed.')
2493 else:
2494 print('OK, will keep Gerrit commit-msg hook in place.')
2495
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002496 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002497 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002498 if options.squash and options.no_squash:
2499 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002500
2501 if not options.squash and not options.no_squash:
2502 # Load default for user, repo, squash=true, in this order.
2503 options.squash = settings.GetSquashGerritUploads()
2504 elif options.no_squash:
2505 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002506
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002507 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002508 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002509
Aaron Gableb56ad332017-01-06 15:24:31 -08002510 # This may be None; default fallback value is determined in logic below.
2511 title = options.title
2512
Dominic Battre7d1c4842017-10-27 09:17:28 +02002513 # Extract bug number from branch name.
2514 bug = options.bug
2515 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2516 if not bug and match:
2517 bug = match.group(1)
2518
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002519 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002520 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002521 if self.GetIssue():
2522 # Try to get the message from a previous upload.
2523 message = self.GetDescription()
2524 if not message:
2525 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002526 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002527 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002528 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002529 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002530 # When uploading a subsequent patchset, -m|--message is taken
2531 # as the patchset title if --title was not provided.
2532 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002533 else:
2534 default_title = RunGit(
2535 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002536 if options.force:
2537 title = default_title
2538 else:
2539 title = ask_for_data(
2540 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002541 change_id = self._GetChangeDetail()['change_id']
2542 while True:
2543 footer_change_ids = git_footers.get_footer_change_id(message)
2544 if footer_change_ids == [change_id]:
2545 break
2546 if not footer_change_ids:
2547 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002548 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002549 continue
2550 # There is already a valid footer but with different or several ids.
2551 # Doing this automatically is non-trivial as we don't want to lose
2552 # existing other footers, yet we want to append just 1 desired
2553 # Change-Id. Thus, just create a new footer, but let user verify the
2554 # new description.
2555 message = '%s\n\nChange-Id: %s' % (message, change_id)
2556 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002557 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002558 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002559 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002560 'Please, check the proposed correction to the description, '
2561 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2562 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2563 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002564 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002565 if not options.force:
2566 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002567 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002568 message = change_desc.description
2569 if not message:
2570 DieWithError("Description is empty. Aborting...")
2571 # Continue the while loop.
2572 # Sanity check of this code - we should end up with proper message
2573 # footer.
2574 assert [change_id] == git_footers.get_footer_change_id(message)
2575 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002576 else: # if not self.GetIssue()
2577 if options.message:
2578 message = options.message
2579 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002580 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002581 if options.title:
2582 message = options.title + '\n\n' + message
2583 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002584
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002585 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002586 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002587 # On first upload, patchset title is always this string, while
2588 # --title flag gets converted to first line of message.
2589 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002590 if not change_desc.description:
2591 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002592 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002593 if len(change_ids) > 1:
2594 DieWithError('too many Change-Id footers, at most 1 allowed.')
2595 if not change_ids:
2596 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002597 change_desc.set_description(git_footers.add_footer_change_id(
2598 change_desc.description,
2599 GenerateGerritChangeId(change_desc.description)))
2600 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002601 assert len(change_ids) == 1
2602 change_id = change_ids[0]
2603
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002604 if options.reviewers or options.tbrs or options.add_owners_to:
2605 change_desc.update_reviewers(options.reviewers, options.tbrs,
2606 options.add_owners_to, change)
2607
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002608 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002609 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2610 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002611 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002612 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2613 desc_tempfile.write(change_desc.description)
2614 desc_tempfile.close()
2615 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2616 '-F', desc_tempfile.name]).strip()
2617 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002618 else:
2619 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002620 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002621 if not change_desc.description:
2622 DieWithError("Description is empty. Aborting...")
2623
2624 if not git_footers.get_footer_change_id(change_desc.description):
2625 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002626 change_desc.set_description(
2627 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002628 if options.reviewers or options.tbrs or options.add_owners_to:
2629 change_desc.update_reviewers(options.reviewers, options.tbrs,
2630 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002631 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002632 # For no-squash mode, we assume the remote called "origin" is the one we
2633 # want. It is not worthwhile to support different workflows for
2634 # no-squash mode.
2635 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002636 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2637
2638 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002639 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002640 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2641 ref_to_push)]).splitlines()
2642 if len(commits) > 1:
2643 print('WARNING: This will upload %d commits. Run the following command '
2644 'to see which commits will be uploaded: ' % len(commits))
2645 print('git log %s..%s' % (parent, ref_to_push))
2646 print('You can also use `git squash-branch` to squash these into a '
2647 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002648 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002649
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002650 if options.reviewers or options.tbrs or options.add_owners_to:
2651 change_desc.update_reviewers(options.reviewers, options.tbrs,
2652 options.add_owners_to, change)
2653
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002654 reviewers = sorted(change_desc.get_reviewers())
2655 # Add cc's from the CC_LIST and --cc flag (if any).
2656 if not options.private and not options.no_autocc:
2657 cc = self.GetCCList().split(',')
2658 else:
2659 cc = []
2660 if options.cc:
2661 cc.extend(options.cc)
2662 cc = filter(None, [email.strip() for email in cc])
2663 if change_desc.get_cced():
2664 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002665 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2666 valid_accounts = set(reviewers + cc)
2667 # TODO(crbug/877717): relax this for all hosts.
2668 else:
2669 valid_accounts = gerrit_util.ValidAccounts(
2670 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002671 logging.info('accounts %s are recognized, %s invalid',
2672 sorted(valid_accounts),
2673 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002674
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002675 # Extra options that can be specified at push time. Doc:
2676 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002677 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002678
Aaron Gable844cf292017-06-28 11:32:59 -07002679 # By default, new changes are started in WIP mode, and subsequent patchsets
2680 # don't send email. At any time, passing --send-mail will mark the change
2681 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002682 if options.send_mail:
2683 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002684 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002685 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002686 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002687 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002688 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002689
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002690 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002691 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002692
Aaron Gable9b713dd2016-12-14 16:04:21 -08002693 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002694 # Punctuation and whitespace in |title| must be percent-encoded.
2695 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002696
agablec6787972016-09-09 16:13:34 -07002697 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002698 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002699
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002700 for r in sorted(reviewers):
2701 if r in valid_accounts:
2702 refspec_opts.append('r=%s' % r)
2703 reviewers.remove(r)
2704 else:
2705 # TODO(tandrii): this should probably be a hard failure.
2706 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2707 % r)
2708 for c in sorted(cc):
2709 # refspec option will be rejected if cc doesn't correspond to an
2710 # account, even though REST call to add such arbitrary cc may succeed.
2711 if c in valid_accounts:
2712 refspec_opts.append('cc=%s' % c)
2713 cc.remove(c)
2714
rmistry9eadede2016-09-19 11:22:43 -07002715 if options.topic:
2716 # Documentation on Gerrit topics is here:
2717 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002718 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002719
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002720 if not change_desc.get_reviewers(tbr_only=True):
2721 # Change is not TBR, so we can inline setting other labels, too.
2722 # TODO(crbug.com/877717): make this working for TBR, too, by figuring out
2723 # max score for CR label somehow.
2724 if options.enable_auto_submit:
2725 refspec_opts.append('l=Auto-Submit+1')
2726 if options.use_commit_queue:
2727 refspec_opts.append('l=Commit-Queue+2')
2728 elif options.cq_dry_run:
2729 refspec_opts.append('l=Commit-Queue+1')
2730
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002731 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002732 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002733 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002734 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002735 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2736
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002737 refspec_suffix = ''
2738 if refspec_opts:
2739 refspec_suffix = '%' + ','.join(refspec_opts)
2740 assert ' ' not in refspec_suffix, (
2741 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2742 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2743
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002744 try:
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002745 before_push = time_time()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002746 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00002747 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemuredcefdc2018-11-08 14:41:42 +00002748 print_stdout=True,
Edward Lemur49c8eaf2018-11-07 22:13:12 +00002749 # Flush after every line: useful for seeing progress when running as
2750 # recipe.
2751 filter_fn=lambda _: sys.stdout.flush())
2752 push_returncode = 0
Edward Lemurfec80c42018-11-01 23:14:14 +00002753 except subprocess2.CalledProcessError as e:
2754 push_returncode = e.returncode
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002755 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002756 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002757 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002758 'credential problems:\n'
2759 ' git cl creds-check\n',
2760 change_desc)
Edward Lemurfec80c42018-11-01 23:14:14 +00002761 finally:
2762 metrics.collector.add_repeated('sub_commands', {
2763 'command': 'git push',
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002764 'execution_time': time_time() - before_push,
Edward Lemurfec80c42018-11-01 23:14:14 +00002765 'exit_code': push_returncode,
2766 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2767 })
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002768
2769 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002770 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002771 change_numbers = [m.group(1)
2772 for m in map(regex.match, push_stdout.splitlines())
2773 if m]
2774 if len(change_numbers) != 1:
2775 DieWithError(
2776 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002777 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002778 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002779 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002780
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002781 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002782 # GetIssue() is not set in case of non-squash uploads according to tests.
2783 # TODO(agable): non-squash uploads in git cl should be removed.
2784 gerrit_util.AddReviewers(
2785 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002786 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002787 reviewers, cc,
2788 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002789
Aaron Gablefd238082017-06-07 13:42:34 -07002790 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09002791 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
2792 score = 1
2793 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
2794 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
2795 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07002796 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002797 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002798 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09002799 msg='Self-approving for TBR',
2800 labels={'Code-Review': score})
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002801 # Labels aren't set through refspec only if tbr is set (see check above).
2802 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
2803 options.cq_dry_run)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002804 return 0
2805
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002806 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2807 change_desc):
2808 """Computes parent of the generated commit to be uploaded to Gerrit.
2809
2810 Returns revision or a ref name.
2811 """
2812 if custom_cl_base:
2813 # Try to avoid creating additional unintended CLs when uploading, unless
2814 # user wants to take this risk.
2815 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2816 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2817 local_ref_of_target_remote])
2818 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002819 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002820 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2821 'If you proceed with upload, more than 1 CL may be created by '
2822 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2823 'If you are certain that specified base `%s` has already been '
2824 'uploaded to Gerrit as another CL, you may proceed.\n' %
2825 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2826 if not force:
2827 confirm_or_exit(
2828 'Do you take responsibility for cleaning up potential mess '
2829 'resulting from proceeding with upload?',
2830 action='upload')
2831 return custom_cl_base
2832
Aaron Gablef97e33d2017-03-30 15:44:27 -07002833 if remote != '.':
2834 return self.GetCommonAncestorWithUpstream()
2835
2836 # If our upstream branch is local, we base our squashed commit on its
2837 # squashed version.
2838 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2839
Aaron Gablef97e33d2017-03-30 15:44:27 -07002840 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002841 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002842
2843 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002844 # TODO(tandrii): consider checking parent change in Gerrit and using its
2845 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2846 # the tree hash of the parent branch. The upside is less likely bogus
2847 # requests to reupload parent change just because it's uploadhash is
2848 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002849 parent = RunGit(['config',
2850 'branch.%s.gerritsquashhash' % upstream_branch_name],
2851 error_ok=True).strip()
2852 # Verify that the upstream branch has been uploaded too, otherwise
2853 # Gerrit will create additional CLs when uploading.
2854 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2855 RunGitSilent(['rev-parse', parent + ':'])):
2856 DieWithError(
2857 '\nUpload upstream branch %s first.\n'
2858 'It is likely that this branch has been rebased since its last '
2859 'upload, so you just need to upload it again.\n'
2860 '(If you uploaded it with --no-squash, then branch dependencies '
2861 'are not supported, and you should reupload with --squash.)'
2862 % upstream_branch_name,
2863 change_desc)
2864 return parent
2865
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002866 def _AddChangeIdToCommitMessage(self, options, args):
2867 """Re-commits using the current message, assumes the commit hook is in
2868 place.
2869 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002870 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002871 git_command = ['commit', '--amend', '-m', log_desc]
2872 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002873 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002874 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002875 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002876 return new_log_desc
2877 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002878 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002879
Ravi Mistry31e7d562018-04-02 12:53:57 -04002880 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2881 """Sets labels on the change based on the provided flags."""
2882 labels = {}
2883 notify = None;
2884 if enable_auto_submit:
2885 labels['Auto-Submit'] = 1
2886 if use_commit_queue:
2887 labels['Commit-Queue'] = 2
2888 elif cq_dry_run:
2889 labels['Commit-Queue'] = 1
2890 notify = False
2891 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002892 gerrit_util.SetReview(
2893 self._GetGerritHost(),
2894 self._GerritChangeIdentifier(),
2895 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04002896
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002897 def SetCQState(self, new_state):
2898 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002899 vote_map = {
2900 _CQState.NONE: 0,
2901 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002902 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002903 }
Aaron Gablefc62f762017-07-17 11:12:07 -07002904 labels = {'Commit-Queue': vote_map[new_state]}
2905 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002906 gerrit_util.SetReview(
2907 self._GetGerritHost(), self._GerritChangeIdentifier(),
2908 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002909
tandriie113dfd2016-10-11 10:20:12 -07002910 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002911 try:
2912 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002913 except GerritChangeNotExists:
2914 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002915
2916 if data['status'] in ('ABANDONED', 'MERGED'):
2917 return 'CL %s is closed' % self.GetIssue()
2918
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002919 def GetTryJobProperties(self, patchset=None):
2920 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002921 data = self._GetChangeDetail(['ALL_REVISIONS'])
2922 patchset = int(patchset or self.GetPatchset())
2923 assert patchset
2924 revision_data = None # Pylint wants it to be defined.
2925 for revision_data in data['revisions'].itervalues():
2926 if int(revision_data['_number']) == patchset:
2927 break
2928 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002929 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002930 (patchset, self.GetIssue()))
2931 return {
2932 'patch_issue': self.GetIssue(),
2933 'patch_set': patchset or self.GetPatchset(),
2934 'patch_project': data['project'],
2935 'patch_storage': 'gerrit',
2936 'patch_ref': revision_data['fetch']['http']['ref'],
2937 'patch_repository_url': revision_data['fetch']['http']['url'],
2938 'patch_gerrit_url': self.GetCodereviewServer(),
2939 }
tandriie113dfd2016-10-11 10:20:12 -07002940
tandriide281ae2016-10-12 06:02:30 -07002941 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002942 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002943
Edward Lemur707d70b2018-02-07 00:50:14 +01002944 def GetReviewers(self):
2945 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002946 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002947
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002948
2949_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002950 'gerrit': _GerritChangelistImpl,
2951}
2952
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002953
iannuccie53c9352016-08-17 14:40:40 -07002954def _add_codereview_issue_select_options(parser, extra=""):
2955 _add_codereview_select_options(parser)
2956
2957 text = ('Operate on this issue number instead of the current branch\'s '
2958 'implicit issue.')
2959 if extra:
2960 text += ' '+extra
2961 parser.add_option('-i', '--issue', type=int, help=text)
2962
2963
2964def _process_codereview_issue_select_options(parser, options):
2965 _process_codereview_select_options(parser, options)
2966 if options.issue is not None and not options.forced_codereview:
2967 parser.error('--issue must be specified with either --rietveld or --gerrit')
2968
2969
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002970def _add_codereview_select_options(parser):
2971 """Appends --gerrit and --rietveld options to force specific codereview."""
2972 parser.codereview_group = optparse.OptionGroup(
2973 parser, 'EXPERIMENTAL! Codereview override options')
2974 parser.add_option_group(parser.codereview_group)
2975 parser.codereview_group.add_option(
2976 '--gerrit', action='store_true',
2977 help='Force the use of Gerrit for codereview')
2978 parser.codereview_group.add_option(
2979 '--rietveld', action='store_true',
2980 help='Force the use of Rietveld for codereview')
2981
2982
2983def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00002984 if options.rietveld:
2985 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002986 options.forced_codereview = None
2987 if options.gerrit:
2988 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002989
2990
tandriif9aefb72016-07-01 09:06:51 -07002991def _get_bug_line_values(default_project, bugs):
2992 """Given default_project and comma separated list of bugs, yields bug line
2993 values.
2994
2995 Each bug can be either:
2996 * a number, which is combined with default_project
2997 * string, which is left as is.
2998
2999 This function may produce more than one line, because bugdroid expects one
3000 project per line.
3001
3002 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3003 ['v8:123', 'chromium:789']
3004 """
3005 default_bugs = []
3006 others = []
3007 for bug in bugs.split(','):
3008 bug = bug.strip()
3009 if bug:
3010 try:
3011 default_bugs.append(int(bug))
3012 except ValueError:
3013 others.append(bug)
3014
3015 if default_bugs:
3016 default_bugs = ','.join(map(str, default_bugs))
3017 if default_project:
3018 yield '%s:%s' % (default_project, default_bugs)
3019 else:
3020 yield default_bugs
3021 for other in sorted(others):
3022 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3023 yield other
3024
3025
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003026class ChangeDescription(object):
3027 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003028 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003029 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003030 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003031 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003032 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3033 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3034 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3035 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003036
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003037 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003038 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003039
agable@chromium.org42c20792013-09-12 17:34:49 +00003040 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003041 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003042 return '\n'.join(self._description_lines)
3043
3044 def set_description(self, desc):
3045 if isinstance(desc, basestring):
3046 lines = desc.splitlines()
3047 else:
3048 lines = [line.rstrip() for line in desc]
3049 while lines and not lines[0]:
3050 lines.pop(0)
3051 while lines and not lines[-1]:
3052 lines.pop(-1)
3053 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003054
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003055 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3056 """Rewrites the R=/TBR= line(s) as a single line each.
3057
3058 Args:
3059 reviewers (list(str)) - list of additional emails to use for reviewers.
3060 tbrs (list(str)) - list of additional emails to use for TBRs.
3061 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3062 the change that are missing OWNER coverage. If this is not None, you
3063 must also pass a value for `change`.
3064 change (Change) - The Change that should be used for OWNERS lookups.
3065 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003066 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003067 assert isinstance(tbrs, list), tbrs
3068
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003069 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003070 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003071
3072 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003073 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003074
3075 reviewers = set(reviewers)
3076 tbrs = set(tbrs)
3077 LOOKUP = {
3078 'TBR': tbrs,
3079 'R': reviewers,
3080 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003081
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003082 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003083 regexp = re.compile(self.R_LINE)
3084 matches = [regexp.match(line) for line in self._description_lines]
3085 new_desc = [l for i, l in enumerate(self._description_lines)
3086 if not matches[i]]
3087 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003088
agable@chromium.org42c20792013-09-12 17:34:49 +00003089 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003090
3091 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003092 for match in matches:
3093 if not match:
3094 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003095 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3096
3097 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003098 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003099 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003100 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003101 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003102 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003103 LOOKUP[add_owners_to].update(
3104 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003105
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003106 # If any folks ended up in both groups, remove them from tbrs.
3107 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003108
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003109 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3110 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003111
3112 # Put the new lines in the description where the old first R= line was.
3113 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3114 if 0 <= line_loc < len(self._description_lines):
3115 if new_tbr_line:
3116 self._description_lines.insert(line_loc, new_tbr_line)
3117 if new_r_line:
3118 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003119 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003120 if new_r_line:
3121 self.append_footer(new_r_line)
3122 if new_tbr_line:
3123 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003124
Aaron Gable3a16ed12017-03-23 10:51:55 -07003125 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003126 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003127 self.set_description([
3128 '# Enter a description of the change.',
3129 '# This will be displayed on the codereview site.',
3130 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003131 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003132 '--------------------',
3133 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003134
agable@chromium.org42c20792013-09-12 17:34:49 +00003135 regexp = re.compile(self.BUG_LINE)
3136 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003137 prefix = settings.GetBugPrefix()
3138 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003139 if git_footer:
3140 self.append_footer('Bug: %s' % ', '.join(values))
3141 else:
3142 for value in values:
3143 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003144
agable@chromium.org42c20792013-09-12 17:34:49 +00003145 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003146 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003147 if not content:
3148 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003149 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003150
Bruce Dawson2377b012018-01-11 16:46:49 -08003151 # Strip off comments and default inserted "Bug:" line.
3152 clean_lines = [line.rstrip() for line in lines if not
3153 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003154 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003155 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003156 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003157
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003158 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003159 """Adds a footer line to the description.
3160
3161 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3162 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3163 that Gerrit footers are always at the end.
3164 """
3165 parsed_footer_line = git_footers.parse_footer(line)
3166 if parsed_footer_line:
3167 # Line is a gerrit footer in the form: Footer-Key: any value.
3168 # Thus, must be appended observing Gerrit footer rules.
3169 self.set_description(
3170 git_footers.add_footer(self.description,
3171 key=parsed_footer_line[0],
3172 value=parsed_footer_line[1]))
3173 return
3174
3175 if not self._description_lines:
3176 self._description_lines.append(line)
3177 return
3178
3179 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3180 if gerrit_footers:
3181 # git_footers.split_footers ensures that there is an empty line before
3182 # actual (gerrit) footers, if any. We have to keep it that way.
3183 assert top_lines and top_lines[-1] == ''
3184 top_lines, separator = top_lines[:-1], top_lines[-1:]
3185 else:
3186 separator = [] # No need for separator if there are no gerrit_footers.
3187
3188 prev_line = top_lines[-1] if top_lines else ''
3189 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3190 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3191 top_lines.append('')
3192 top_lines.append(line)
3193 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003194
tandrii99a72f22016-08-17 14:33:24 -07003195 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003196 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003197 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003198 reviewers = [match.group(2).strip()
3199 for match in matches
3200 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003201 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003202
bradnelsond975b302016-10-23 12:20:23 -07003203 def get_cced(self):
3204 """Retrieves the list of reviewers."""
3205 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3206 cced = [match.group(2).strip() for match in matches if match]
3207 return cleanup_list(cced)
3208
Nodir Turakulov23b82142017-11-16 11:04:25 -08003209 def get_hash_tags(self):
3210 """Extracts and sanitizes a list of Gerrit hashtags."""
3211 subject = (self._description_lines or ('',))[0]
3212 subject = re.sub(
3213 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3214
3215 tags = []
3216 start = 0
3217 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3218 while True:
3219 m = bracket_exp.match(subject, start)
3220 if not m:
3221 break
3222 tags.append(self.sanitize_hash_tag(m.group(1)))
3223 start = m.end()
3224
3225 if not tags:
3226 # Try "Tag: " prefix.
3227 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3228 if m:
3229 tags.append(self.sanitize_hash_tag(m.group(1)))
3230 return tags
3231
3232 @classmethod
3233 def sanitize_hash_tag(cls, tag):
3234 """Returns a sanitized Gerrit hash tag.
3235
3236 A sanitized hashtag can be used as a git push refspec parameter value.
3237 """
3238 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3239
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003240 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3241 """Updates this commit description given the parent.
3242
3243 This is essentially what Gnumbd used to do.
3244 Consult https://goo.gl/WMmpDe for more details.
3245 """
3246 assert parent_msg # No, orphan branch creation isn't supported.
3247 assert parent_hash
3248 assert dest_ref
3249 parent_footer_map = git_footers.parse_footers(parent_msg)
3250 # This will also happily parse svn-position, which GnumbD is no longer
3251 # supporting. While we'd generate correct footers, the verifier plugin
3252 # installed in Gerrit will block such commit (ie git push below will fail).
3253 parent_position = git_footers.get_position(parent_footer_map)
3254
3255 # Cherry-picks may have last line obscuring their prior footers,
3256 # from git_footers perspective. This is also what Gnumbd did.
3257 cp_line = None
3258 if (self._description_lines and
3259 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3260 cp_line = self._description_lines.pop()
3261
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003262 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003263
3264 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3265 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003266 for i, line in enumerate(footer_lines):
3267 k, v = git_footers.parse_footer(line) or (None, None)
3268 if k and k.startswith('Cr-'):
3269 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003270
3271 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003272 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003273 if parent_position[0] == dest_ref:
3274 # Same branch as parent.
3275 number = int(parent_position[1]) + 1
3276 else:
3277 number = 1 # New branch, and extra lineage.
3278 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3279 int(parent_position[1])))
3280
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003281 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3282 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003283
3284 self._description_lines = top_lines
3285 if cp_line:
3286 self._description_lines.append(cp_line)
3287 if self._description_lines[-1] != '':
3288 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003289 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003290
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003291
Aaron Gablea1bab272017-04-11 16:38:18 -07003292def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003293 """Retrieves the reviewers that approved a CL from the issue properties with
3294 messages.
3295
3296 Note that the list may contain reviewers that are not committer, thus are not
3297 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003298
3299 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003300 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003301 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003302 return sorted(
3303 set(
3304 message['sender']
3305 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003306 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003307 )
3308 )
3309
3310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003311def FindCodereviewSettingsFile(filename='codereview.settings'):
3312 """Finds the given file starting in the cwd and going up.
3313
3314 Only looks up to the top of the repository unless an
3315 'inherit-review-settings-ok' file exists in the root of the repository.
3316 """
3317 inherit_ok_file = 'inherit-review-settings-ok'
3318 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003319 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003320 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3321 root = '/'
3322 while True:
3323 if filename in os.listdir(cwd):
3324 if os.path.isfile(os.path.join(cwd, filename)):
3325 return open(os.path.join(cwd, filename))
3326 if cwd == root:
3327 break
3328 cwd = os.path.dirname(cwd)
3329
3330
3331def LoadCodereviewSettingsFromFile(fileobj):
3332 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003333 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003334
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003335 def SetProperty(name, setting, unset_error_ok=False):
3336 fullname = 'rietveld.' + name
3337 if setting in keyvals:
3338 RunGit(['config', fullname, keyvals[setting]])
3339 else:
3340 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3341
tandrii48df5812016-10-17 03:55:37 -07003342 if not keyvals.get('GERRIT_HOST', False):
3343 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003344 # Only server setting is required. Other settings can be absent.
3345 # In that case, we ignore errors raised during option deletion attempt.
3346 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003347 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003348 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3349 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003350 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003351 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3352 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003353 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003354 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3355 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003356
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003357 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003358 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003359
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003360 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003361 RunGit(['config', 'gerrit.squash-uploads',
3362 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003363
tandrii@chromium.org28253532016-04-14 13:46:56 +00003364 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003365 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003366 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3367
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003368 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003369 # should be of the form
3370 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3371 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003372 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3373 keyvals['ORIGIN_URL_CONFIG']])
3374
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003375
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003376def urlretrieve(source, destination):
3377 """urllib is broken for SSL connections via a proxy therefore we
3378 can't use urllib.urlretrieve()."""
3379 with open(destination, 'w') as f:
3380 f.write(urllib2.urlopen(source).read())
3381
3382
ukai@chromium.org712d6102013-11-27 00:52:58 +00003383def hasSheBang(fname):
3384 """Checks fname is a #! script."""
3385 with open(fname) as f:
3386 return f.read(2).startswith('#!')
3387
3388
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003389# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3390def DownloadHooks(*args, **kwargs):
3391 pass
3392
3393
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003394def DownloadGerritHook(force):
3395 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003396
3397 Args:
3398 force: True to update hooks. False to install hooks if not present.
3399 """
3400 if not settings.GetIsGerrit():
3401 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003402 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003403 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3404 if not os.access(dst, os.X_OK):
3405 if os.path.exists(dst):
3406 if not force:
3407 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003408 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003409 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003410 if not hasSheBang(dst):
3411 DieWithError('Not a script: %s\n'
3412 'You need to download from\n%s\n'
3413 'into .git/hooks/commit-msg and '
3414 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003415 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3416 except Exception:
3417 if os.path.exists(dst):
3418 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003419 DieWithError('\nFailed to download hooks.\n'
3420 'You need to download from\n%s\n'
3421 'into .git/hooks/commit-msg and '
3422 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003423
3424
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003425def GetRietveldCodereviewSettingsInteractively():
3426 """Prompt the user for settings."""
3427 server = settings.GetDefaultServerUrl(error_ok=True)
3428 prompt = 'Rietveld server (host[:port])'
3429 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3430 newserver = ask_for_data(prompt + ':')
3431 if not server and not newserver:
3432 newserver = DEFAULT_SERVER
3433 if newserver:
3434 newserver = gclient_utils.UpgradeToHttps(newserver)
3435 if newserver != server:
3436 RunGit(['config', 'rietveld.server', newserver])
3437
3438 def SetProperty(initial, caption, name, is_url):
3439 prompt = caption
3440 if initial:
3441 prompt += ' ("x" to clear) [%s]' % initial
3442 new_val = ask_for_data(prompt + ':')
3443 if new_val == 'x':
3444 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3445 elif new_val:
3446 if is_url:
3447 new_val = gclient_utils.UpgradeToHttps(new_val)
3448 if new_val != initial:
3449 RunGit(['config', 'rietveld.' + name, new_val])
3450
3451 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3452 SetProperty(settings.GetDefaultPrivateFlag(),
3453 'Private flag (rietveld only)', 'private', False)
3454 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3455 'tree-status-url', False)
3456 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3457 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3458 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3459 'run-post-upload-hook', False)
3460
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003461
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003462class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003463 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003464
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003465 _GOOGLESOURCE = 'googlesource.com'
3466
3467 def __init__(self):
3468 # Cached list of [host, identity, source], where source is either
3469 # .gitcookies or .netrc.
3470 self._all_hosts = None
3471
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003472 def ensure_configured_gitcookies(self):
3473 """Runs checks and suggests fixes to make git use .gitcookies from default
3474 path."""
3475 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3476 configured_path = RunGitSilent(
3477 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003478 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003479 if configured_path:
3480 self._ensure_default_gitcookies_path(configured_path, default)
3481 else:
3482 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003483
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003484 @staticmethod
3485 def _ensure_default_gitcookies_path(configured_path, default_path):
3486 assert configured_path
3487 if configured_path == default_path:
3488 print('git is already configured to use your .gitcookies from %s' %
3489 configured_path)
3490 return
3491
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003492 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003493 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3494 (configured_path, default_path))
3495
3496 if not os.path.exists(configured_path):
3497 print('However, your configured .gitcookies file is missing.')
3498 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3499 action='reconfigure')
3500 RunGit(['config', '--global', 'http.cookiefile', default_path])
3501 return
3502
3503 if os.path.exists(default_path):
3504 print('WARNING: default .gitcookies file already exists %s' %
3505 default_path)
3506 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3507 default_path)
3508
3509 confirm_or_exit('Move existing .gitcookies to default location?',
3510 action='move')
3511 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003512 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003513 print('Moved and reconfigured git to use .gitcookies from %s' %
3514 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003515
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003516 @staticmethod
3517 def _configure_gitcookies_path(default_path):
3518 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3519 if os.path.exists(netrc_path):
3520 print('You seem to be using outdated .netrc for git credentials: %s' %
3521 netrc_path)
3522 print('This tool will guide you through setting up recommended '
3523 '.gitcookies store for git credentials.\n'
3524 '\n'
3525 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3526 ' git config --global --unset http.cookiefile\n'
3527 ' mv %s %s.backup\n\n' % (default_path, default_path))
3528 confirm_or_exit(action='setup .gitcookies')
3529 RunGit(['config', '--global', 'http.cookiefile', default_path])
3530 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003531
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003532 def get_hosts_with_creds(self, include_netrc=False):
3533 if self._all_hosts is None:
3534 a = gerrit_util.CookiesAuthenticator()
3535 self._all_hosts = [
3536 (h, u, s)
3537 for h, u, s in itertools.chain(
3538 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3539 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3540 )
3541 if h.endswith(self._GOOGLESOURCE)
3542 ]
3543
3544 if include_netrc:
3545 return self._all_hosts
3546 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3547
3548 def print_current_creds(self, include_netrc=False):
3549 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3550 if not hosts:
3551 print('No Git/Gerrit credentials found')
3552 return
3553 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3554 header = [('Host', 'User', 'Which file'),
3555 ['=' * l for l in lengths]]
3556 for row in (header + hosts):
3557 print('\t'.join((('%%+%ds' % l) % s)
3558 for l, s in zip(lengths, row)))
3559
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003560 @staticmethod
3561 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003562 """Parses identity "git-<username>.domain" into <username> and domain."""
3563 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003564 # distinguishable from sub-domains. But we do know typical domains:
3565 if identity.endswith('.chromium.org'):
3566 domain = 'chromium.org'
3567 username = identity[:-len('.chromium.org')]
3568 else:
3569 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003570 if username.startswith('git-'):
3571 username = username[len('git-'):]
3572 return username, domain
3573
3574 def _get_usernames_of_domain(self, domain):
3575 """Returns list of usernames referenced by .gitcookies in a given domain."""
3576 identities_by_domain = {}
3577 for _, identity, _ in self.get_hosts_with_creds():
3578 username, domain = self._parse_identity(identity)
3579 identities_by_domain.setdefault(domain, []).append(username)
3580 return identities_by_domain.get(domain)
3581
3582 def _canonical_git_googlesource_host(self, host):
3583 """Normalizes Gerrit hosts (with '-review') to Git host."""
3584 assert host.endswith(self._GOOGLESOURCE)
3585 # Prefix doesn't include '.' at the end.
3586 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3587 if prefix.endswith('-review'):
3588 prefix = prefix[:-len('-review')]
3589 return prefix + '.' + self._GOOGLESOURCE
3590
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003591 def _canonical_gerrit_googlesource_host(self, host):
3592 git_host = self._canonical_git_googlesource_host(host)
3593 prefix = git_host.split('.', 1)[0]
3594 return prefix + '-review.' + self._GOOGLESOURCE
3595
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003596 def _get_counterpart_host(self, host):
3597 assert host.endswith(self._GOOGLESOURCE)
3598 git = self._canonical_git_googlesource_host(host)
3599 gerrit = self._canonical_gerrit_googlesource_host(git)
3600 return git if gerrit == host else gerrit
3601
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003602 def has_generic_host(self):
3603 """Returns whether generic .googlesource.com has been configured.
3604
3605 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3606 """
3607 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3608 if host == '.' + self._GOOGLESOURCE:
3609 return True
3610 return False
3611
3612 def _get_git_gerrit_identity_pairs(self):
3613 """Returns map from canonic host to pair of identities (Git, Gerrit).
3614
3615 One of identities might be None, meaning not configured.
3616 """
3617 host_to_identity_pairs = {}
3618 for host, identity, _ in self.get_hosts_with_creds():
3619 canonical = self._canonical_git_googlesource_host(host)
3620 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3621 idx = 0 if canonical == host else 1
3622 pair[idx] = identity
3623 return host_to_identity_pairs
3624
3625 def get_partially_configured_hosts(self):
3626 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003627 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3628 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3629 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003630
3631 def get_conflicting_hosts(self):
3632 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003633 host
3634 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003635 if None not in (i1, i2) and i1 != i2)
3636
3637 def get_duplicated_hosts(self):
3638 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3639 return set(host for host, count in counters.iteritems() if count > 1)
3640
3641 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3642 'chromium.googlesource.com': 'chromium.org',
3643 'chrome-internal.googlesource.com': 'google.com',
3644 }
3645
3646 def get_hosts_with_wrong_identities(self):
3647 """Finds hosts which **likely** reference wrong identities.
3648
3649 Note: skips hosts which have conflicting identities for Git and Gerrit.
3650 """
3651 hosts = set()
3652 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3653 pair = self._get_git_gerrit_identity_pairs().get(host)
3654 if pair and pair[0] == pair[1]:
3655 _, domain = self._parse_identity(pair[0])
3656 if domain != expected:
3657 hosts.add(host)
3658 return hosts
3659
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003660 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003661 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003662 hosts = sorted(hosts)
3663 assert hosts
3664 if extra_column_func is None:
3665 extras = [''] * len(hosts)
3666 else:
3667 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003668 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3669 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003670 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003671 lines.append(tmpl % he)
3672 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003673
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003674 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003675 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003676 yield ('.googlesource.com wildcard record detected',
3677 ['Chrome Infrastructure team recommends to list full host names '
3678 'explicitly.'],
3679 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003680
3681 dups = self.get_duplicated_hosts()
3682 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003683 yield ('The following hosts were defined twice',
3684 self._format_hosts(dups),
3685 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003686
3687 partial = self.get_partially_configured_hosts()
3688 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003689 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3690 'These hosts are missing',
3691 self._format_hosts(partial, lambda host: 'but %s defined' %
3692 self._get_counterpart_host(host)),
3693 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003694
3695 conflicting = self.get_conflicting_hosts()
3696 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003697 yield ('The following Git hosts have differing credentials from their '
3698 'Gerrit counterparts',
3699 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3700 tuple(self._get_git_gerrit_identity_pairs()[host])),
3701 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003702
3703 wrong = self.get_hosts_with_wrong_identities()
3704 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003705 yield ('These hosts likely use wrong identity',
3706 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3707 (self._get_git_gerrit_identity_pairs()[host][0],
3708 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3709 wrong)
3710
3711 def find_and_report_problems(self):
3712 """Returns True if there was at least one problem, else False."""
3713 found = False
3714 bad_hosts = set()
3715 for title, sublines, hosts in self._find_problems():
3716 if not found:
3717 found = True
3718 print('\n\n.gitcookies problem report:\n')
3719 bad_hosts.update(hosts or [])
3720 print(' %s%s' % (title , (':' if sublines else '')))
3721 if sublines:
3722 print()
3723 print(' %s' % '\n '.join(sublines))
3724 print()
3725
3726 if bad_hosts:
3727 assert found
3728 print(' You can manually remove corresponding lines in your %s file and '
3729 'visit the following URLs with correct account to generate '
3730 'correct credential lines:\n' %
3731 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3732 print(' %s' % '\n '.join(sorted(set(
3733 gerrit_util.CookiesAuthenticator().get_new_password_url(
3734 self._canonical_git_googlesource_host(host))
3735 for host in bad_hosts
3736 ))))
3737 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003738
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003739
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003740@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003741def CMDcreds_check(parser, args):
3742 """Checks credentials and suggests changes."""
3743 _, _ = parser.parse_args(args)
3744
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003745 # Code below checks .gitcookies. Abort if using something else.
3746 authn = gerrit_util.Authenticator.get()
3747 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3748 if isinstance(authn, gerrit_util.GceAuthenticator):
3749 DieWithError(
3750 'This command is not designed for GCE, are you on a bot?\n'
3751 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3752 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003753 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003754 'This command is not designed for bot environment. It checks '
3755 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003756
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003757 checker = _GitCookiesChecker()
3758 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003759
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003760 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003761 checker.print_current_creds(include_netrc=True)
3762
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003763 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003764 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003765 return 0
3766 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003767
3768
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003769@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003770@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003771def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003772 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003773
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003774 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07003775 # TODO(tandrii): remove this once we switch to Gerrit.
3776 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003777 parser.add_option('--activate-update', action='store_true',
3778 help='activate auto-updating [rietveld] section in '
3779 '.git/config')
3780 parser.add_option('--deactivate-update', action='store_true',
3781 help='deactivate auto-updating [rietveld] section in '
3782 '.git/config')
3783 options, args = parser.parse_args(args)
3784
3785 if options.deactivate_update:
3786 RunGit(['config', 'rietveld.autoupdate', 'false'])
3787 return
3788
3789 if options.activate_update:
3790 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3791 return
3792
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003793 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003794 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003795 return 0
3796
3797 url = args[0]
3798 if not url.endswith('codereview.settings'):
3799 url = os.path.join(url, 'codereview.settings')
3800
3801 # Load code review settings and download hooks (if available).
3802 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3803 return 0
3804
3805
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003806@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003807def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003808 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003809 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3810 branch = ShortBranchName(branchref)
3811 _, args = parser.parse_args(args)
3812 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003813 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003814 return RunGit(['config', 'branch.%s.base-url' % branch],
3815 error_ok=False).strip()
3816 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003817 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003818 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3819 error_ok=False).strip()
3820
3821
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003822def color_for_status(status):
3823 """Maps a Changelist status to color, for CMDstatus and other tools."""
3824 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003825 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003826 'waiting': Fore.BLUE,
3827 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003828 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003829 'lgtm': Fore.GREEN,
3830 'commit': Fore.MAGENTA,
3831 'closed': Fore.CYAN,
3832 'error': Fore.WHITE,
3833 }.get(status, Fore.WHITE)
3834
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003835
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003836def get_cl_statuses(changes, fine_grained, max_processes=None):
3837 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003838
3839 If fine_grained is true, this will fetch CL statuses from the server.
3840 Otherwise, simply indicate if there's a matching url for the given branches.
3841
3842 If max_processes is specified, it is used as the maximum number of processes
3843 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3844 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003845
3846 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003847 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003848 if not changes:
3849 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003850
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003851 if not fine_grained:
3852 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003853 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003854 for cl in changes:
3855 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003856 return
3857
3858 # First, sort out authentication issues.
3859 logging.debug('ensuring credentials exist')
3860 for cl in changes:
3861 cl.EnsureAuthenticated(force=False, refresh=True)
3862
3863 def fetch(cl):
3864 try:
3865 return (cl, cl.GetStatus())
3866 except:
3867 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003868 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003869 raise
3870
3871 threads_count = len(changes)
3872 if max_processes:
3873 threads_count = max(1, min(threads_count, max_processes))
3874 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3875
3876 pool = ThreadPool(threads_count)
3877 fetched_cls = set()
3878 try:
3879 it = pool.imap_unordered(fetch, changes).__iter__()
3880 while True:
3881 try:
3882 cl, status = it.next(timeout=5)
3883 except multiprocessing.TimeoutError:
3884 break
3885 fetched_cls.add(cl)
3886 yield cl, status
3887 finally:
3888 pool.close()
3889
3890 # Add any branches that failed to fetch.
3891 for cl in set(changes) - fetched_cls:
3892 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003893
rmistry@google.com2dd99862015-06-22 12:22:18 +00003894
3895def upload_branch_deps(cl, args):
3896 """Uploads CLs of local branches that are dependents of the current branch.
3897
3898 If the local branch dependency tree looks like:
3899 test1 -> test2.1 -> test3.1
3900 -> test3.2
3901 -> test2.2 -> test3.3
3902
3903 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3904 run on the dependent branches in this order:
3905 test2.1, test3.1, test3.2, test2.2, test3.3
3906
3907 Note: This function does not rebase your local dependent branches. Use it when
3908 you make a change to the parent branch that will not conflict with its
3909 dependent branches, and you would like their dependencies updated in
3910 Rietveld.
3911 """
3912 if git_common.is_dirty_git_tree('upload-branch-deps'):
3913 return 1
3914
3915 root_branch = cl.GetBranch()
3916 if root_branch is None:
3917 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3918 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003919 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003920 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3921 'patchset dependencies without an uploaded CL.')
3922
3923 branches = RunGit(['for-each-ref',
3924 '--format=%(refname:short) %(upstream:short)',
3925 'refs/heads'])
3926 if not branches:
3927 print('No local branches found.')
3928 return 0
3929
3930 # Create a dictionary of all local branches to the branches that are dependent
3931 # on it.
3932 tracked_to_dependents = collections.defaultdict(list)
3933 for b in branches.splitlines():
3934 tokens = b.split()
3935 if len(tokens) == 2:
3936 branch_name, tracked = tokens
3937 tracked_to_dependents[tracked].append(branch_name)
3938
vapiera7fbd5a2016-06-16 09:17:49 -07003939 print()
3940 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003941 dependents = []
3942 def traverse_dependents_preorder(branch, padding=''):
3943 dependents_to_process = tracked_to_dependents.get(branch, [])
3944 padding += ' '
3945 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003946 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003947 dependents.append(dependent)
3948 traverse_dependents_preorder(dependent, padding)
3949 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003950 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003951
3952 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003953 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003954 return 0
3955
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003956 confirm_or_exit('This command will checkout all dependent branches and run '
3957 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003958
rmistry@google.com2dd99862015-06-22 12:22:18 +00003959 # Record all dependents that failed to upload.
3960 failures = {}
3961 # Go through all dependents, checkout the branch and upload.
3962 try:
3963 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003964 print()
3965 print('--------------------------------------')
3966 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003967 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003968 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003969 try:
3970 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003971 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003972 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003973 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003974 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003975 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003976 finally:
3977 # Swap back to the original root branch.
3978 RunGit(['checkout', '-q', root_branch])
3979
vapiera7fbd5a2016-06-16 09:17:49 -07003980 print()
3981 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003982 for dependent_branch in dependents:
3983 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003984 print(' %s : %s' % (dependent_branch, upload_status))
3985 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003986
3987 return 0
3988
3989
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003990@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003991def CMDarchive(parser, args):
3992 """Archives and deletes branches associated with closed changelists."""
3993 parser.add_option(
3994 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003995 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003996 parser.add_option(
3997 '-f', '--force', action='store_true',
3998 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003999 parser.add_option(
4000 '-d', '--dry-run', action='store_true',
4001 help='Skip the branch tagging and removal steps.')
4002 parser.add_option(
4003 '-t', '--notags', action='store_true',
4004 help='Do not tag archived branches. '
4005 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004006
4007 auth.add_auth_options(parser)
4008 options, args = parser.parse_args(args)
4009 if args:
4010 parser.error('Unsupported args: %s' % ' '.join(args))
4011 auth_config = auth.extract_auth_config_from_options(options)
4012
4013 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4014 if not branches:
4015 return 0
4016
vapiera7fbd5a2016-06-16 09:17:49 -07004017 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004018 changes = [Changelist(branchref=b, auth_config=auth_config)
4019 for b in branches.splitlines()]
4020 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4021 statuses = get_cl_statuses(changes,
4022 fine_grained=True,
4023 max_processes=options.maxjobs)
4024 proposal = [(cl.GetBranch(),
4025 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4026 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00004027 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07004028 proposal.sort()
4029
4030 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004031 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004032 return 0
4033
4034 current_branch = GetCurrentBranch()
4035
vapiera7fbd5a2016-06-16 09:17:49 -07004036 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004037 if options.notags:
4038 for next_item in proposal:
4039 print(' ' + next_item[0])
4040 else:
4041 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4042 for next_item in proposal:
4043 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004044
kmarshall9249e012016-08-23 12:02:16 -07004045 # Quit now on precondition failure or if instructed by the user, either
4046 # via an interactive prompt or by command line flags.
4047 if options.dry_run:
4048 print('\nNo changes were made (dry run).\n')
4049 return 0
4050 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004051 print('You are currently on a branch \'%s\' which is associated with a '
4052 'closed codereview issue, so archive cannot proceed. Please '
4053 'checkout another branch and run this command again.' %
4054 current_branch)
4055 return 1
kmarshall9249e012016-08-23 12:02:16 -07004056 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004057 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4058 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004059 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004060 return 1
4061
4062 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004063 if not options.notags:
4064 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004065 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004066
vapiera7fbd5a2016-06-16 09:17:49 -07004067 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004068
4069 return 0
4070
4071
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004072@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004073def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004074 """Show status of changelists.
4075
4076 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004077 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004078 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004079 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004080 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004081 - Magenta in the commit queue
4082 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004083 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004084
4085 Also see 'git cl comments'.
4086 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004087 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004088 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004089 parser.add_option('-f', '--fast', action='store_true',
4090 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004091 parser.add_option(
4092 '-j', '--maxjobs', action='store', type=int,
4093 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004094
4095 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004096 _add_codereview_issue_select_options(
4097 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004098 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004099 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004100 if args:
4101 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004102 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004103
iannuccie53c9352016-08-17 14:40:40 -07004104 if options.issue is not None and not options.field:
4105 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004106
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004107 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004108 cl = Changelist(auth_config=auth_config, issue=options.issue,
4109 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004110 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004111 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004112 elif options.field == 'id':
4113 issueid = cl.GetIssue()
4114 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004115 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004116 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004117 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004118 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004119 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004120 elif options.field == 'status':
4121 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004122 elif options.field == 'url':
4123 url = cl.GetIssueURL()
4124 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004125 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004126 return 0
4127
4128 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4129 if not branches:
4130 print('No local branch found.')
4131 return 0
4132
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004133 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004134 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004135 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004136 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004137 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004138 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004139 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004140
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004141 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004142 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4143 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4144 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004145 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004146 c, status = output.next()
4147 branch_statuses[c.GetBranch()] = status
4148 status = branch_statuses.pop(branch)
4149 url = cl.GetIssueURL()
4150 if url and (not status or status == 'error'):
4151 # The issue probably doesn't exist anymore.
4152 url += ' (broken)'
4153
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004154 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004155 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004156 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004157 color = ''
4158 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004159 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004160 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004161 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004162 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004163
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004164
4165 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004166 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004167 print('Current branch: %s' % branch)
4168 for cl in changes:
4169 if cl.GetBranch() == branch:
4170 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004171 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004172 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004173 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004174 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004175 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004176 print('Issue description:')
4177 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004178 return 0
4179
4180
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004181def colorize_CMDstatus_doc():
4182 """To be called once in main() to add colors to git cl status help."""
4183 colors = [i for i in dir(Fore) if i[0].isupper()]
4184
4185 def colorize_line(line):
4186 for color in colors:
4187 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004188 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004189 indent = len(line) - len(line.lstrip(' ')) + 1
4190 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4191 return line
4192
4193 lines = CMDstatus.__doc__.splitlines()
4194 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4195
4196
phajdan.jre328cf92016-08-22 04:12:17 -07004197def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004198 if path == '-':
4199 json.dump(contents, sys.stdout)
4200 else:
4201 with open(path, 'w') as f:
4202 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004203
4204
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004205@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004206@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004207def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004208 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004209
4210 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004211 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004212 parser.add_option('-r', '--reverse', action='store_true',
4213 help='Lookup the branch(es) for the specified issues. If '
4214 'no issues are specified, all branches with mapped '
4215 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004216 parser.add_option('--json',
4217 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004218 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004219 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004220 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004221
dnj@chromium.org406c4402015-03-03 17:22:28 +00004222 if options.reverse:
4223 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004224 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004225 # Reverse issue lookup.
4226 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004227
4228 git_config = {}
4229 for config in RunGit(['config', '--get-regexp',
4230 r'branch\..*issue']).splitlines():
4231 name, _space, val = config.partition(' ')
4232 git_config[name] = val
4233
dnj@chromium.org406c4402015-03-03 17:22:28 +00004234 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004235 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4236 config_key = _git_branch_config_key(ShortBranchName(branch),
4237 cls.IssueConfigKey())
4238 issue = git_config.get(config_key)
4239 if issue:
4240 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004241 if not args:
4242 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004243 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004244 for issue in args:
4245 if not issue:
4246 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004247 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004248 print('Branch for issue number %s: %s' % (
4249 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004250 if options.json:
4251 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004252 return 0
4253
4254 if len(args) > 0:
4255 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4256 if not issue.valid:
4257 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4258 'or no argument to list it.\n'
4259 'Maybe you want to run git cl status?')
4260 cl = Changelist(codereview=issue.codereview)
4261 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004262 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004263 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004264 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4265 if options.json:
4266 write_json(options.json, {
4267 'issue': cl.GetIssue(),
4268 'issue_url': cl.GetIssueURL(),
4269 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004270 return 0
4271
4272
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004273@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004274def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004275 """Shows or posts review comments for any changelist."""
4276 parser.add_option('-a', '--add-comment', dest='comment',
4277 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004278 parser.add_option('-p', '--publish', action='store_true',
4279 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004280 parser.add_option('-i', '--issue', dest='issue',
4281 help='review issue id (defaults to current issue). '
4282 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004283 parser.add_option('-m', '--machine-readable', dest='readable',
4284 action='store_false', default=True,
4285 help='output comments in a format compatible with '
4286 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004287 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004288 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004289 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004290 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004291 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004292 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004293 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004294
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004295 issue = None
4296 if options.issue:
4297 try:
4298 issue = int(options.issue)
4299 except ValueError:
4300 DieWithError('A review issue id is expected to be a number')
4301
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004302 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4303
4304 if not cl.IsGerrit():
4305 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004306
4307 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004308 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004309 return 0
4310
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004311 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4312 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004313 for comment in summary:
4314 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004315 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004316 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004317 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004318 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004319 color = Fore.MAGENTA
4320 else:
4321 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004322 print('\n%s%s %s%s\n%s' % (
4323 color,
4324 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4325 comment.sender,
4326 Fore.RESET,
4327 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4328
smut@google.comc85ac942015-09-15 16:34:43 +00004329 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004330 def pre_serialize(c):
4331 dct = c.__dict__.copy()
4332 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4333 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004334 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004335 return 0
4336
4337
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004338@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004339@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004340def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004341 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004342 parser.add_option('-d', '--display', action='store_true',
4343 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004344 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004345 help='New description to set for this issue (- for stdin, '
4346 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004347 parser.add_option('-f', '--force', action='store_true',
4348 help='Delete any unpublished Gerrit edits for this issue '
4349 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004350
4351 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004352 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004353 options, args = parser.parse_args(args)
4354 _process_codereview_select_options(parser, options)
4355
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004356 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004357 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004358 target_issue_arg = ParseIssueNumberArgument(args[0],
4359 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004360 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004361 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004362
martiniss6eda05f2016-06-30 10:18:35 -07004363 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004364 'auth_config': auth.extract_auth_config_from_options(options),
4365 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004366 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004367 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004368 if target_issue_arg:
4369 kwargs['issue'] = target_issue_arg.issue
4370 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004371 if target_issue_arg.codereview and not options.forced_codereview:
4372 detected_codereview_from_url = True
4373 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004374
4375 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004376 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004377 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004378 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004379
4380 if detected_codereview_from_url:
4381 logging.info('canonical issue/change URL: %s (type: %s)\n',
4382 cl.GetIssueURL(), target_issue_arg.codereview)
4383
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004384 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004385
smut@google.com34fb6b12015-07-13 20:03:26 +00004386 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004387 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004388 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004389
4390 if options.new_description:
4391 text = options.new_description
4392 if text == '-':
4393 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004394 elif text == '+':
4395 base_branch = cl.GetCommonAncestorWithUpstream()
4396 change = cl.GetChange(base_branch, None, local_description=True)
4397 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004398
4399 description.set_description(text)
4400 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004401 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004402
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004403 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004404 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004405 return 0
4406
4407
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004408@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004409def CMDlint(parser, args):
4410 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004411 parser.add_option('--filter', action='append', metavar='-x,+y',
4412 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004413 auth.add_auth_options(parser)
4414 options, args = parser.parse_args(args)
4415 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004416
4417 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004418 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004419 try:
4420 import cpplint
4421 import cpplint_chromium
4422 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004423 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004424 return 1
4425
4426 # Change the current working directory before calling lint so that it
4427 # shows the correct base.
4428 previous_cwd = os.getcwd()
4429 os.chdir(settings.GetRoot())
4430 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004431 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004432 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4433 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004434 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004435 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004436 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004437
4438 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004439 command = args + files
4440 if options.filter:
4441 command = ['--filter=' + ','.join(options.filter)] + command
4442 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004443
4444 white_regex = re.compile(settings.GetLintRegex())
4445 black_regex = re.compile(settings.GetLintIgnoreRegex())
4446 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4447 for filename in filenames:
4448 if white_regex.match(filename):
4449 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004450 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004451 else:
4452 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4453 extra_check_functions)
4454 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004455 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004456 finally:
4457 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004458 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004459 if cpplint._cpplint_state.error_count != 0:
4460 return 1
4461 return 0
4462
4463
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004464@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004465def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004466 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004467 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004468 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004469 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004470 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004471 parser.add_option('--all', action='store_true',
4472 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004473 parser.add_option('--parallel', action='store_true',
4474 help='Run all tests specified by input_api.RunTests in all '
4475 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004476 auth.add_auth_options(parser)
4477 options, args = parser.parse_args(args)
4478 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004479
sbc@chromium.org71437c02015-04-09 19:29:40 +00004480 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004481 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004482 return 1
4483
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004484 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004485 if args:
4486 base_branch = args[0]
4487 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004488 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004489 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004490
Aaron Gable8076c282017-11-29 14:39:41 -08004491 if options.all:
4492 base_change = cl.GetChange(base_branch, None)
4493 files = [('M', f) for f in base_change.AllFiles()]
4494 change = presubmit_support.GitChange(
4495 base_change.Name(),
4496 base_change.FullDescriptionText(),
4497 base_change.RepositoryRoot(),
4498 files,
4499 base_change.issue,
4500 base_change.patchset,
4501 base_change.author_email,
4502 base_change._upstream)
4503 else:
4504 change = cl.GetChange(base_branch, None)
4505
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004506 cl.RunHook(
4507 committing=not options.upload,
4508 may_prompt=False,
4509 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004510 change=change,
4511 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004512 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004513
4514
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004515def GenerateGerritChangeId(message):
4516 """Returns Ixxxxxx...xxx change id.
4517
4518 Works the same way as
4519 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4520 but can be called on demand on all platforms.
4521
4522 The basic idea is to generate git hash of a state of the tree, original commit
4523 message, author/committer info and timestamps.
4524 """
4525 lines = []
4526 tree_hash = RunGitSilent(['write-tree'])
4527 lines.append('tree %s' % tree_hash.strip())
4528 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4529 if code == 0:
4530 lines.append('parent %s' % parent.strip())
4531 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4532 lines.append('author %s' % author.strip())
4533 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4534 lines.append('committer %s' % committer.strip())
4535 lines.append('')
4536 # Note: Gerrit's commit-hook actually cleans message of some lines and
4537 # whitespace. This code is not doing this, but it clearly won't decrease
4538 # entropy.
4539 lines.append(message)
4540 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4541 stdin='\n'.join(lines))
4542 return 'I%s' % change_hash.strip()
4543
4544
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004545def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004546 """Computes the remote branch ref to use for the CL.
4547
4548 Args:
4549 remote (str): The git remote for the CL.
4550 remote_branch (str): The git remote branch for the CL.
4551 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004552 """
4553 if not (remote and remote_branch):
4554 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004555
wittman@chromium.org455dc922015-01-26 20:15:50 +00004556 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004557 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004558 # refs, which are then translated into the remote full symbolic refs
4559 # below.
4560 if '/' not in target_branch:
4561 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4562 else:
4563 prefix_replacements = (
4564 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4565 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4566 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4567 )
4568 match = None
4569 for regex, replacement in prefix_replacements:
4570 match = re.search(regex, target_branch)
4571 if match:
4572 remote_branch = target_branch.replace(match.group(0), replacement)
4573 break
4574 if not match:
4575 # This is a branch path but not one we recognize; use as-is.
4576 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004577 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4578 # Handle the refs that need to land in different refs.
4579 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004580
wittman@chromium.org455dc922015-01-26 20:15:50 +00004581 # Create the true path to the remote branch.
4582 # Does the following translation:
4583 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4584 # * refs/remotes/origin/master -> refs/heads/master
4585 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4586 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4587 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4588 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4589 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4590 'refs/heads/')
4591 elif remote_branch.startswith('refs/remotes/branch-heads'):
4592 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004593
wittman@chromium.org455dc922015-01-26 20:15:50 +00004594 return remote_branch
4595
4596
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004597def cleanup_list(l):
4598 """Fixes a list so that comma separated items are put as individual items.
4599
4600 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4601 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4602 """
4603 items = sum((i.split(',') for i in l), [])
4604 stripped_items = (i.strip() for i in items)
4605 return sorted(filter(None, stripped_items))
4606
4607
Aaron Gable4db38df2017-11-03 14:59:07 -07004608@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004609@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004610def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004611 """Uploads the current changelist to codereview.
4612
4613 Can skip dependency patchset uploads for a branch by running:
4614 git config branch.branch_name.skip-deps-uploads True
4615 To unset run:
4616 git config --unset branch.branch_name.skip-deps-uploads
4617 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004618
4619 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4620 a bug number, this bug number is automatically populated in the CL
4621 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004622
4623 If subject contains text in square brackets or has "<text>: " prefix, such
4624 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4625 [git-cl] add support for hashtags
4626 Foo bar: implement foo
4627 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004628 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004629 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4630 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004631 parser.add_option('--bypass-watchlists', action='store_true',
4632 dest='bypass_watchlists',
4633 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004634 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004635 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004636 parser.add_option('--message', '-m', dest='message',
4637 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004638 parser.add_option('-b', '--bug',
4639 help='pre-populate the bug number(s) for this issue. '
4640 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004641 parser.add_option('--message-file', dest='message_file',
4642 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004643 parser.add_option('--title', '-t', dest='title',
4644 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004645 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004646 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004647 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004648 parser.add_option('--tbrs',
4649 action='append', default=[],
4650 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004651 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004652 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004653 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004654 parser.add_option('--hashtag', dest='hashtags',
4655 action='append', default=[],
4656 help=('Gerrit hashtag for new CL; '
4657 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004658 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004659 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004660 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004661 help='tell the commit queue to commit this patchset; '
4662 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004663 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004664 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004665 metavar='TARGET',
4666 help='Apply CL to remote ref TARGET. ' +
4667 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004668 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004669 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004670 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004671 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004672 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004673 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004674 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4675 const='TBR', help='add a set of OWNERS to TBR')
4676 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4677 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004678 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4679 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004680 help='Send the patchset to do a CQ dry run right after '
4681 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004682 parser.add_option('--dependencies', action='store_true',
4683 help='Uploads CLs of all the local branches that depend on '
4684 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004685 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4686 help='Sends your change to the CQ after an approval. Only '
4687 'works on repos that have the Auto-Submit label '
4688 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004689 parser.add_option('--parallel', action='store_true',
4690 help='Run all tests specified by input_api.RunTests in all '
4691 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004692
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004693 parser.add_option('--no-autocc', action='store_true',
4694 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004695 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004696 help='Set the review private. This implies --no-autocc.')
4697
rmistry@google.com2dd99862015-06-22 12:22:18 +00004698 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004699 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004700 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004701 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004702 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004703 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004704
sbc@chromium.org71437c02015-04-09 19:29:40 +00004705 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004706 return 1
4707
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004708 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004709 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004710 options.cc = cleanup_list(options.cc)
4711
tandriib80458a2016-06-23 12:20:07 -07004712 if options.message_file:
4713 if options.message:
4714 parser.error('only one of --message and --message-file allowed.')
4715 options.message = gclient_utils.FileRead(options.message_file)
4716 options.message_file = None
4717
tandrii4d0545a2016-07-06 03:56:49 -07004718 if options.cq_dry_run and options.use_commit_queue:
4719 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4720
Aaron Gableedbc4132017-09-11 13:22:28 -07004721 if options.use_commit_queue:
4722 options.send_mail = True
4723
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004724 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4725 settings.GetIsGerrit()
4726
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004727 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004728 if not cl.IsGerrit():
4729 # Error out with instructions for repos not yet configured for Gerrit.
4730 print('=====================================')
4731 print('NOTICE: Rietveld is no longer supported. '
4732 'You can upload changes to Gerrit with')
4733 print(' git cl upload --gerrit')
4734 print('or set Gerrit to be your default code review tool with')
4735 print(' git config gerrit.host true')
4736 print('=====================================')
4737 return 1
4738
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004739 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004740
4741
Francois Dorayd42c6812017-05-30 15:10:20 -04004742@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004743@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004744def CMDsplit(parser, args):
4745 """Splits a branch into smaller branches and uploads CLs.
4746
4747 Creates a branch and uploads a CL for each group of files modified in the
4748 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004749 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004750 the shared OWNERS file.
4751 """
4752 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004753 help="A text file containing a CL description in which "
4754 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004755 parser.add_option("-c", "--comment", dest="comment_file",
4756 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004757 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4758 default=False,
4759 help="List the files and reviewers for each CL that would "
4760 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004761 parser.add_option("--cq-dry-run", action='store_true',
4762 help="If set, will do a cq dry run for each uploaded CL. "
4763 "Please be careful when doing this; more than ~10 CLs "
4764 "has the potential to overload our build "
4765 "infrastructure. Try to upload these not during high "
4766 "load times (usually 11-3 Mountain View time). Email "
4767 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004768 options, _ = parser.parse_args(args)
4769
4770 if not options.description_file:
4771 parser.error('No --description flag specified.')
4772
4773 def WrappedCMDupload(args):
4774 return CMDupload(OptionParser(), args)
4775
4776 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004777 Changelist, WrappedCMDupload, options.dry_run,
4778 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04004779
4780
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004781@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004782@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004783def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004784 """DEPRECATED: Used to commit the current changelist via git-svn."""
4785 message = ('git-cl no longer supports committing to SVN repositories via '
4786 'git-svn. You probably want to use `git cl land` instead.')
4787 print(message)
4788 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004789
4790
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004791# Two special branches used by git cl land.
4792MERGE_BRANCH = 'git-cl-commit'
4793CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4794
4795
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004796@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004797@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004798def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004799 """Commits the current changelist via git.
4800
4801 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4802 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004803 """
4804 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4805 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004806 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004807 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004808 parser.add_option('--parallel', action='store_true',
4809 help='Run all tests specified by input_api.RunTests in all '
4810 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004811 auth.add_auth_options(parser)
4812 (options, args) = parser.parse_args(args)
4813 auth_config = auth.extract_auth_config_from_options(options)
4814
4815 cl = Changelist(auth_config=auth_config)
4816
Robert Iannucci2e73d432018-03-14 01:10:47 -07004817 if not cl.IsGerrit():
4818 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004819
Robert Iannucci2e73d432018-03-14 01:10:47 -07004820 if not cl.GetIssue():
4821 DieWithError('You must upload the change first to Gerrit.\n'
4822 ' If you would rather have `git cl land` upload '
4823 'automatically for you, see http://crbug.com/642759')
4824 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004825 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004826
4827
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004828@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004829@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004830def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004831 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004832 parser.add_option('-b', dest='newbranch',
4833 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004834 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004835 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004836 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004837 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004838 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004839 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004840 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004841 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004842 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004843 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004844
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004845
4846 group = optparse.OptionGroup(
4847 parser,
4848 'Options for continuing work on the current issue uploaded from a '
4849 'different clone (e.g. different machine). Must be used independently '
4850 'from the other options. No issue number should be specified, and the '
4851 'branch must have an issue number associated with it')
4852 group.add_option('--reapply', action='store_true', dest='reapply',
4853 help='Reset the branch and reapply the issue.\n'
4854 'CAUTION: This will undo any local changes in this '
4855 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004856
4857 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004858 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004859 parser.add_option_group(group)
4860
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004861 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004862 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004863 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004864 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004865 auth_config = auth.extract_auth_config_from_options(options)
4866
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004867 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004868 if options.newbranch:
4869 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004870 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004871 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004872
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004873 cl = Changelist(auth_config=auth_config,
4874 codereview=options.forced_codereview)
4875 if not cl.GetIssue():
4876 parser.error('current branch must have an associated issue')
4877
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004878 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004879 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004880 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004881
4882 RunGit(['reset', '--hard', upstream])
4883 if options.pull:
4884 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004885
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004886 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4887 options.directory)
4888
4889 if len(args) != 1 or not args[0]:
4890 parser.error('Must specify issue number or url')
4891
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004892 target_issue_arg = ParseIssueNumberArgument(args[0],
4893 options.forced_codereview)
4894 if not target_issue_arg.valid:
4895 parser.error('invalid codereview url or CL id')
4896
4897 cl_kwargs = {
4898 'auth_config': auth_config,
4899 'codereview_host': target_issue_arg.hostname,
4900 'codereview': options.forced_codereview,
4901 }
4902 detected_codereview_from_url = False
4903 if target_issue_arg.codereview and not options.forced_codereview:
4904 detected_codereview_from_url = True
4905 cl_kwargs['codereview'] = target_issue_arg.codereview
4906 cl_kwargs['issue'] = target_issue_arg.issue
4907
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004908 # We don't want uncommitted changes mixed up with the patch.
4909 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004910 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004911
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004912 if options.newbranch:
4913 if options.force:
4914 RunGit(['branch', '-D', options.newbranch],
4915 stderr=subprocess2.PIPE, error_ok=True)
4916 RunGit(['new-branch', options.newbranch])
4917
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004918 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004919
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004920 if cl.IsGerrit():
4921 if options.reject:
4922 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004923 if options.directory:
4924 parser.error('--directory is not supported with Gerrit codereview.')
4925
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004926 if detected_codereview_from_url:
4927 print('canonical issue/change URL: %s (type: %s)\n' %
4928 (cl.GetIssueURL(), target_issue_arg.codereview))
4929
4930 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07004931 options.nocommit, options.directory,
4932 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004933
4934
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004935def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004936 """Fetches the tree status and returns either 'open', 'closed',
4937 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004938 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004939 if url:
4940 status = urllib2.urlopen(url).read().lower()
4941 if status.find('closed') != -1 or status == '0':
4942 return 'closed'
4943 elif status.find('open') != -1 or status == '1':
4944 return 'open'
4945 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004946 return 'unset'
4947
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004948
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004949def GetTreeStatusReason():
4950 """Fetches the tree status from a json url and returns the message
4951 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004952 url = settings.GetTreeStatusUrl()
4953 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004954 connection = urllib2.urlopen(json_url)
4955 status = json.loads(connection.read())
4956 connection.close()
4957 return status['message']
4958
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004959
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004960@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004961def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004962 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004963 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004964 status = GetTreeStatus()
4965 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004966 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004967 return 2
4968
vapiera7fbd5a2016-06-16 09:17:49 -07004969 print('The tree is %s' % status)
4970 print()
4971 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004972 if status != 'open':
4973 return 1
4974 return 0
4975
4976
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004977@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004978def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004979 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004980 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004981 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004982 '-b', '--bot', action='append',
4983 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4984 'times to specify multiple builders. ex: '
4985 '"-b win_rel -b win_layout". See '
4986 'the try server waterfall for the builders name and the tests '
4987 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004988 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004989 '-B', '--bucket', default='',
4990 help=('Buildbucket bucket to send the try requests.'))
4991 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004992 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004993 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004994 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004995 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004996 help='Revision to use for the try job; default: the revision will '
4997 'be determined by the try recipe that builder runs, which usually '
4998 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004999 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005000 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005001 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005002 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005003 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005004 '--category', default='git_cl_try', help='Specify custom build category.')
5005 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005006 '--project',
5007 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005008 'in recipe to determine to which repository or directory to '
5009 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005010 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005011 '-p', '--property', dest='properties', action='append', default=[],
5012 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005013 'key2=value2 etc. The value will be treated as '
5014 'json if decodable, or as string otherwise. '
5015 'NOTE: using this may make your try job not usable for CQ, '
5016 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005017 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005018 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5019 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005020 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005021 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005022 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005023 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005024 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005025 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005026
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005027 if options.master and options.master.startswith('luci.'):
5028 parser.error(
5029 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005030 # Make sure that all properties are prop=value pairs.
5031 bad_params = [x for x in options.properties if '=' not in x]
5032 if bad_params:
5033 parser.error('Got properties with missing "=": %s' % bad_params)
5034
maruel@chromium.org15192402012-09-06 12:38:29 +00005035 if args:
5036 parser.error('Unknown arguments: %s' % args)
5037
Koji Ishii31c14782018-01-08 17:17:33 +09005038 cl = Changelist(auth_config=auth_config, issue=options.issue,
5039 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005040 if not cl.GetIssue():
5041 parser.error('Need to upload first')
5042
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005043 if cl.IsGerrit():
5044 # HACK: warm up Gerrit change detail cache to save on RPCs.
5045 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5046
tandriie113dfd2016-10-11 10:20:12 -07005047 error_message = cl.CannotTriggerTryJobReason()
5048 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005049 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005050
borenet6c0efe62016-10-19 08:13:29 -07005051 if options.bucket and options.master:
5052 parser.error('Only one of --bucket and --master may be used.')
5053
qyearsley1fdfcb62016-10-24 13:22:03 -07005054 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005055
qyearsleydd49f942016-10-28 11:57:22 -07005056 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5057 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005058 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005059 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005060 print('git cl try with no bots now defaults to CQ dry run.')
5061 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5062 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005063
borenet6c0efe62016-10-19 08:13:29 -07005064 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005065 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005066 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005067 'of bot requires an initial job from a parent (usually a builder). '
5068 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005069 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005070 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005071
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005072 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005073 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005074 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005075 except BuildbucketResponseException as ex:
5076 print('ERROR: %s' % ex)
5077 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005078 return 0
5079
5080
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005081@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005082def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005083 """Prints info about try jobs associated with current CL."""
5084 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005085 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005086 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005087 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005088 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005089 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005090 '--color', action='store_true', default=setup_color.IS_TTY,
5091 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005092 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005093 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5094 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005095 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005096 '--json', help=('Path of JSON output file to write try job results to,'
5097 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005098 parser.add_option_group(group)
5099 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005100 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005101 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005102 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005103 if args:
5104 parser.error('Unrecognized args: %s' % ' '.join(args))
5105
5106 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005107 cl = Changelist(
5108 issue=options.issue, codereview=options.forced_codereview,
5109 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005110 if not cl.GetIssue():
5111 parser.error('Need to upload first')
5112
tandrii221ab252016-10-06 08:12:04 -07005113 patchset = options.patchset
5114 if not patchset:
5115 patchset = cl.GetMostRecentPatchset()
5116 if not patchset:
5117 parser.error('Codereview doesn\'t know about issue %s. '
5118 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005119 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005120 cl.GetIssue())
5121
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005122 try:
tandrii221ab252016-10-06 08:12:04 -07005123 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005124 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005125 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005126 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005127 if options.json:
5128 write_try_results_json(options.json, jobs)
5129 else:
5130 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005131 return 0
5132
5133
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005134@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005135@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005136def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005137 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005138 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005139 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005140 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005141
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005142 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005143 if args:
5144 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005145 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005146 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005147 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005148 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005149
5150 # Clear configured merge-base, if there is one.
5151 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005152 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005153 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005154 return 0
5155
5156
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005157@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005158def CMDweb(parser, args):
5159 """Opens the current CL in the web browser."""
5160 _, args = parser.parse_args(args)
5161 if args:
5162 parser.error('Unrecognized args: %s' % ' '.join(args))
5163
5164 issue_url = Changelist().GetIssueURL()
5165 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005166 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005167 return 1
5168
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005169 # Redirect I/O before invoking browser to hide its output. For example, this
5170 # allows to hide "Created new window in existing browser session." message
5171 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5172 saved_stdout = os.dup(1)
5173 os.close(1)
5174 os.open(os.devnull, os.O_RDWR)
5175 try:
5176 webbrowser.open(issue_url)
5177 finally:
5178 os.dup2(saved_stdout, 1)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005179 return 0
5180
5181
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005182@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005183def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005184 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005185 parser.add_option('-d', '--dry-run', action='store_true',
5186 help='trigger in dry run mode')
5187 parser.add_option('-c', '--clear', action='store_true',
5188 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005189 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005190 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005191 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005192 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005193 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005194 if args:
5195 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005196 if options.dry_run and options.clear:
5197 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5198
iannuccie53c9352016-08-17 14:40:40 -07005199 cl = Changelist(auth_config=auth_config, issue=options.issue,
5200 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005201 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005202 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005203 elif options.dry_run:
5204 state = _CQState.DRY_RUN
5205 else:
5206 state = _CQState.COMMIT
5207 if not cl.GetIssue():
5208 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005209 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005210 return 0
5211
5212
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005213@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005214def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005215 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005216 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005217 auth.add_auth_options(parser)
5218 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005219 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005220 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005221 if args:
5222 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005223 cl = Changelist(auth_config=auth_config, issue=options.issue,
5224 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005225 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005226 if not cl.GetIssue():
5227 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005228 cl.CloseIssue()
5229 return 0
5230
5231
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005232@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005233def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005234 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005235 parser.add_option(
5236 '--stat',
5237 action='store_true',
5238 dest='stat',
5239 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005240 auth.add_auth_options(parser)
5241 options, args = parser.parse_args(args)
5242 auth_config = auth.extract_auth_config_from_options(options)
5243 if args:
5244 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005245
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005246 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005247 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005248 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005249 if not issue:
5250 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005251
Aaron Gablea718c3e2017-08-28 17:47:28 -07005252 base = cl._GitGetBranchConfigValue('last-upload-hash')
5253 if not base:
5254 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5255 if not base:
5256 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5257 revision_info = detail['revisions'][detail['current_revision']]
5258 fetch_info = revision_info['fetch']['http']
5259 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5260 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005261
Aaron Gablea718c3e2017-08-28 17:47:28 -07005262 cmd = ['git', 'diff']
5263 if options.stat:
5264 cmd.append('--stat')
5265 cmd.append(base)
5266 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005267
5268 return 0
5269
5270
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005271@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005272def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005273 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005274 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005275 '--ignore-current',
5276 action='store_true',
5277 help='Ignore the CL\'s current reviewers and start from scratch.')
5278 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005279 '--no-color',
5280 action='store_true',
5281 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005282 parser.add_option(
5283 '--batch',
5284 action='store_true',
5285 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005286 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005287 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005288 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005289
5290 author = RunGit(['config', 'user.email']).strip() or None
5291
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005292 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005293
5294 if args:
5295 if len(args) > 1:
5296 parser.error('Unknown args')
5297 base_branch = args[0]
5298 else:
5299 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005300 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005301
5302 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005303 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5304
5305 if options.batch:
5306 db = owners.Database(change.RepositoryRoot(), file, os.path)
5307 print('\n'.join(db.reviewers_for(affected_files, author)))
5308 return 0
5309
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005310 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005311 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005312 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005313 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005314 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005315 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005316 disable_color=options.no_color,
5317 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005318
5319
Aiden Bennerc08566e2018-10-03 17:52:42 +00005320def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005321 """Generates a diff command."""
5322 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005323 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5324
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005325 if allow_prefix:
5326 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5327 # case that diff.noprefix is set in the user's git config.
5328 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5329 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005330 diff_cmd += ['--no-prefix']
5331
5332 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005333
5334 if args:
5335 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005336 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005337 diff_cmd.append(arg)
5338 else:
5339 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005340
5341 return diff_cmd
5342
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005343
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005344def MatchingFileType(file_name, extensions):
5345 """Returns true if the file name ends with one of the given extensions."""
5346 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005347
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005348
enne@chromium.org555cfe42014-01-29 18:21:39 +00005349@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005350@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005351def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005352 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005353 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005354 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005355 parser.add_option('--full', action='store_true',
5356 help='Reformat the full content of all touched files')
5357 parser.add_option('--dry-run', action='store_true',
5358 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005359 parser.add_option(
5360 '--python',
5361 action='store_true',
5362 default=None,
5363 help='Enables python formatting on all python files.')
5364 parser.add_option(
5365 '--no-python',
5366 action='store_true',
5367 dest='python',
5368 help='Disables python formatting on all python files. '
5369 'Takes precedence over --python. '
5370 'If neither --python or --no-python are set, python '
5371 'files that have a .style.yapf file in an ancestor '
5372 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005373 parser.add_option('--js', action='store_true',
5374 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005375 parser.add_option('--diff', action='store_true',
5376 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005377 parser.add_option('--presubmit', action='store_true',
5378 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005379 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005380
Daniel Chengc55eecf2016-12-30 03:11:02 -08005381 # Normalize any remaining args against the current path, so paths relative to
5382 # the current directory are still resolved as expected.
5383 args = [os.path.join(os.getcwd(), arg) for arg in args]
5384
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005385 # git diff generates paths against the root of the repository. Change
5386 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005387 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005388 if rel_base_path:
5389 os.chdir(rel_base_path)
5390
digit@chromium.org29e47272013-05-17 17:01:46 +00005391 # Grab the merge-base commit, i.e. the upstream commit of the current
5392 # branch when it was created or the last time it was rebased. This is
5393 # to cover the case where the user may have called "git fetch origin",
5394 # moving the origin branch to a newer commit, but hasn't rebased yet.
5395 upstream_commit = None
5396 cl = Changelist()
5397 upstream_branch = cl.GetUpstreamBranch()
5398 if upstream_branch:
5399 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5400 upstream_commit = upstream_commit.strip()
5401
5402 if not upstream_commit:
5403 DieWithError('Could not find base commit for this branch. '
5404 'Are you in detached state?')
5405
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005406 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5407 diff_output = RunGit(changed_files_cmd)
5408 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005409 # Filter out files deleted by this CL
5410 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005411
Christopher Lamc5ba6922017-01-24 11:19:14 +11005412 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005413 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005414
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005415 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5416 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5417 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005418 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005419
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005420 top_dir = os.path.normpath(
5421 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5422
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005423 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5424 # formatted. This is used to block during the presubmit.
5425 return_value = 0
5426
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005427 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005428 # Locate the clang-format binary in the checkout
5429 try:
5430 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005431 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005432 DieWithError(e)
5433
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005434 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005435 cmd = [clang_format_tool]
5436 if not opts.dry_run and not opts.diff:
5437 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005438 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005439 if opts.diff:
5440 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005441 else:
5442 env = os.environ.copy()
5443 env['PATH'] = str(os.path.dirname(clang_format_tool))
5444 try:
5445 script = clang_format.FindClangFormatScriptInChromiumTree(
5446 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005447 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005448 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005449
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005450 cmd = [sys.executable, script, '-p0']
5451 if not opts.dry_run and not opts.diff:
5452 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005453
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005454 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5455 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005456
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005457 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5458 if opts.diff:
5459 sys.stdout.write(stdout)
5460 if opts.dry_run and len(stdout) > 0:
5461 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005462
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005463 # Similar code to above, but using yapf on .py files rather than clang-format
5464 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005465 py_explicitly_disabled = opts.python is not None and not opts.python
5466 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005467 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5468 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5469 if sys.platform.startswith('win'):
5470 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005471
Aiden Bennerc08566e2018-10-03 17:52:42 +00005472 # If we couldn't find a yapf file we'll default to the chromium style
5473 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005474 chromium_default_yapf_style = os.path.join(depot_tools_path,
5475 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005476 # Used for caching.
5477 yapf_configs = {}
5478 for f in python_diff_files:
5479 # Find the yapf style config for the current file, defaults to depot
5480 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005481 _FindYapfConfigFile(f, yapf_configs, top_dir)
5482
5483 # Turn on python formatting by default if a yapf config is specified.
5484 # This breaks in the case of this repo though since the specified
5485 # style file is also the global default.
5486 if opts.python is None:
5487 filtered_py_files = []
5488 for f in python_diff_files:
5489 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5490 filtered_py_files.append(f)
5491 else:
5492 filtered_py_files = python_diff_files
5493
5494 # Note: yapf still seems to fix indentation of the entire file
5495 # even if line ranges are specified.
5496 # See https://github.com/google/yapf/issues/499
5497 if not opts.full and filtered_py_files:
5498 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5499
5500 for f in filtered_py_files:
5501 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5502 if yapf_config is None:
5503 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005504
5505 cmd = [yapf_tool, '--style', yapf_config, f]
5506
5507 has_formattable_lines = False
5508 if not opts.full:
5509 # Only run yapf over changed line ranges.
5510 for diff_start, diff_len in py_line_diffs[f]:
5511 diff_end = diff_start + diff_len - 1
5512 # Yapf errors out if diff_end < diff_start but this
5513 # is a valid line range diff for a removal.
5514 if diff_end >= diff_start:
5515 has_formattable_lines = True
5516 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5517 # If all line diffs were removals we have nothing to format.
5518 if not has_formattable_lines:
5519 continue
5520
5521 if opts.diff or opts.dry_run:
5522 cmd += ['--diff']
5523 # Will return non-zero exit code if non-empty diff.
5524 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5525 if opts.diff:
5526 sys.stdout.write(stdout)
5527 elif len(stdout) > 0:
5528 return_value = 2
5529 else:
5530 cmd += ['-i']
5531 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005532
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005533 # Dart's formatter does not have the nice property of only operating on
5534 # modified chunks, so hard code full.
5535 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005536 try:
5537 command = [dart_format.FindDartFmtToolInChromiumTree()]
5538 if not opts.dry_run and not opts.diff:
5539 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005540 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005541
ppi@chromium.org6593d932016-03-03 15:41:15 +00005542 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005543 if opts.dry_run and stdout:
5544 return_value = 2
5545 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005546 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5547 'found in this checkout. Files in other languages are still '
5548 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005549
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005550 # Format GN build files. Always run on full build files for canonical form.
5551 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005552 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005553 if opts.dry_run or opts.diff:
5554 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005555 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005556 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5557 shell=sys.platform == 'win32',
5558 cwd=top_dir)
5559 if opts.dry_run and gn_ret == 2:
5560 return_value = 2 # Not formatted.
5561 elif opts.diff and gn_ret == 2:
5562 # TODO this should compute and print the actual diff.
5563 print("This change has GN build file diff for " + gn_diff_file)
5564 elif gn_ret != 0:
5565 # For non-dry run cases (and non-2 return values for dry-run), a
5566 # nonzero error code indicates a failure, probably because the file
5567 # doesn't parse.
5568 DieWithError("gn format failed on " + gn_diff_file +
5569 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005570
Ilya Shermane081cbe2017-08-15 17:51:04 -07005571 # Skip the metrics formatting from the global presubmit hook. These files have
5572 # a separate presubmit hook that issues an error if the files need formatting,
5573 # whereas the top-level presubmit script merely issues a warning. Formatting
5574 # these files is somewhat slow, so it's important not to duplicate the work.
5575 if not opts.presubmit:
5576 for xml_dir in GetDirtyMetricsDirs(diff_files):
5577 tool_dir = os.path.join(top_dir, xml_dir)
5578 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5579 if opts.dry_run or opts.diff:
5580 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005581 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005582 if opts.diff:
5583 sys.stdout.write(stdout)
5584 if opts.dry_run and stdout:
5585 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005586
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005587 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005588
Steven Holte2e664bf2017-04-21 13:10:47 -07005589def GetDirtyMetricsDirs(diff_files):
5590 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5591 metrics_xml_dirs = [
5592 os.path.join('tools', 'metrics', 'actions'),
5593 os.path.join('tools', 'metrics', 'histograms'),
5594 os.path.join('tools', 'metrics', 'rappor'),
5595 os.path.join('tools', 'metrics', 'ukm')]
5596 for xml_dir in metrics_xml_dirs:
5597 if any(file.startswith(xml_dir) for file in xml_diff_files):
5598 yield xml_dir
5599
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005600
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005601@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005602@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005603def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005604 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005605 _, args = parser.parse_args(args)
5606
5607 if len(args) != 1:
5608 parser.print_help()
5609 return 1
5610
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005611 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005612 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005613 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005614
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005615 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005616
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005617 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005618 output = RunGit(['config', '--local', '--get-regexp',
5619 r'branch\..*\.%s' % issueprefix],
5620 error_ok=True)
5621 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005622 if issue == target_issue:
5623 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005624
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005625 branches = []
5626 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005627 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005628 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005629 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005630 return 1
5631 if len(branches) == 1:
5632 RunGit(['checkout', branches[0]])
5633 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005634 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005635 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005636 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005637 which = raw_input('Choose by index: ')
5638 try:
5639 RunGit(['checkout', branches[int(which)]])
5640 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005641 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005642 return 1
5643
5644 return 0
5645
5646
maruel@chromium.org29404b52014-09-08 22:58:00 +00005647def CMDlol(parser, args):
5648 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005649 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005650 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5651 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5652 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005653 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005654 return 0
5655
5656
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005657class OptionParser(optparse.OptionParser):
5658 """Creates the option parse and add --verbose support."""
5659 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005660 optparse.OptionParser.__init__(
5661 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005662 self.add_option(
5663 '-v', '--verbose', action='count', default=0,
5664 help='Use 2 times for more debugging info')
5665
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005666 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005667 try:
5668 return self._parse_args(args)
5669 finally:
5670 # Regardless of success or failure of args parsing, we want to report
5671 # metrics, but only after logging has been initialized (if parsing
5672 # succeeded).
5673 global settings
5674 settings = Settings()
5675
5676 if not metrics.DISABLE_METRICS_COLLECTION:
5677 # GetViewVCUrl ultimately calls logging method.
5678 project_url = settings.GetViewVCUrl().strip('/+')
5679 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5680 metrics.collector.add('project_urls', [project_url])
5681
5682 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005683 # Create an optparse.Values object that will store only the actual passed
5684 # options, without the defaults.
5685 actual_options = optparse.Values()
5686 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5687 # Create an optparse.Values object with the default options.
5688 options = optparse.Values(self.get_default_values().__dict__)
5689 # Update it with the options passed by the user.
5690 options._update_careful(actual_options.__dict__)
5691 # Store the options passed by the user in an _actual_options attribute.
5692 # We store only the keys, and not the values, since the values can contain
5693 # arbitrary information, which might be PII.
5694 metrics.collector.add('arguments', actual_options.__dict__.keys())
5695
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005696 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005697 logging.basicConfig(
5698 level=levels[min(options.verbose, len(levels) - 1)],
5699 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5700 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005701
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005702 return options, args
5703
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005704
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005705def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005706 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005707 print('\nYour python version %s is unsupported, please upgrade.\n' %
5708 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005709 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005710
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005711 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005712 dispatcher = subcommand.CommandDispatcher(__name__)
5713 try:
5714 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005715 except auth.AuthenticationError as e:
5716 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005717 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005718 if e.code != 500:
5719 raise
5720 DieWithError(
5721 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5722 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005723 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005724
5725
5726if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005727 # These affect sys.stdout so do it outside of main() to simplify mocks in
5728 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005729 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005730 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005731 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005732 sys.exit(main(sys.argv[1:]))