blob: b400c10937486798f95cbc11c0aa00ee882d75b3 [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
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and 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
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000034import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000035import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000036import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000037import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038
39try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080040 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041except ImportError:
42 pass
43
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000044from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000045from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046from third_party import upload
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
piman@chromium.org336f9122014-09-04 02:16:55 +000059import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000060import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000062import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import 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
borenet6c0efe62016-10-19 08:13:29 -070084# Buildbucket master name prefix.
85MASTER_PREFIX = 'master.'
86
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000087# Shortcut since it quickly becomes redundant.
88Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000089
maruel@chromium.orgddd59412011-11-30 14:20:38 +000090# Initialized in main()
91settings = None
92
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010093# Used by tests/git_cl_test.py to add extra logging.
94# Inside the weirdly failing test, add this:
95# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -070096# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010097_IS_BEING_TESTED = False
98
maruel@chromium.orgddd59412011-11-30 14:20:38 +000099
Christopher Lamf732cd52017-01-24 12:40:11 +1100100def DieWithError(message, change_desc=None):
101 if change_desc:
102 SaveDescriptionBackup(change_desc)
103
vapiera7fbd5a2016-06-16 09:17:49 -0700104 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105 sys.exit(1)
106
107
Christopher Lamf732cd52017-01-24 12:40:11 +1100108def SaveDescriptionBackup(change_desc):
109 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000110 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100111 backup_file = open(backup_path, 'w')
112 backup_file.write(change_desc.description)
113 backup_file.close()
114
115
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000116def GetNoGitPagerEnv():
117 env = os.environ.copy()
118 # 'cat' is a magical git string that disables pagers on all platforms.
119 env['GIT_PAGER'] = 'cat'
120 return env
121
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000122
bsep@chromium.org627d9002016-04-29 00:00:52 +0000123def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000124 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000125 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000126 except subprocess2.CalledProcessError as e:
127 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000128 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000130 'Command "%s" failed.\n%s' % (
131 ' '.join(args), error_message or e.stdout or ''))
132 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
135def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000137 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000138
139
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000140def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000141 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700142 if suppress_stderr:
143 stderr = subprocess2.VOID
144 else:
145 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000146 try:
tandrii5d48c322016-08-18 16:19:37 -0700147 (out, _), code = subprocess2.communicate(['git'] + args,
148 env=GetNoGitPagerEnv(),
149 stdout=subprocess2.PIPE,
150 stderr=stderr)
151 return code, out
152 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900153 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700154 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000155
156
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000157def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000158 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000159 return RunGitWithCode(args, suppress_stderr=True)[1]
160
161
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000163 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000164 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000165 return (version.startswith(prefix) and
166 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000167
168
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000169def BranchExists(branch):
170 """Return True if specified branch exists."""
171 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
172 suppress_stderr=True)
173 return not code
174
175
tandrii2a16b952016-10-19 07:09:44 -0700176def time_sleep(seconds):
177 # Use this so that it can be mocked in tests without interfering with python
178 # system machinery.
179 import time # Local import to discourage others from importing time globally.
180 return time.sleep(seconds)
181
182
maruel@chromium.org90541732011-04-01 17:54:18 +0000183def ask_for_data(prompt):
184 try:
185 return raw_input(prompt)
186 except KeyboardInterrupt:
187 # Hide the exception.
188 sys.exit(1)
189
190
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100191def confirm_or_exit(prefix='', action='confirm'):
192 """Asks user to press enter to continue or press Ctrl+C to abort."""
193 if not prefix or prefix.endswith('\n'):
194 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100195 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100196 mid = ' Press'
197 elif prefix.endswith(' '):
198 mid = 'press'
199 else:
200 mid = ' press'
201 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
202
203
204def ask_for_explicit_yes(prompt):
205 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
206 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
207 while True:
208 if 'yes'.startswith(result):
209 return True
210 if 'no'.startswith(result):
211 return False
212 result = ask_for_data('Please, type yes or no: ').lower()
213
214
tandrii5d48c322016-08-18 16:19:37 -0700215def _git_branch_config_key(branch, key):
216 """Helper method to return Git config key for a branch."""
217 assert branch, 'branch name is required to set git config for it'
218 return 'branch.%s.%s' % (branch, key)
219
220
221def _git_get_branch_config_value(key, default=None, value_type=str,
222 branch=False):
223 """Returns git config value of given or current branch if any.
224
225 Returns default in all other cases.
226 """
227 assert value_type in (int, str, bool)
228 if branch is False: # Distinguishing default arg value from None.
229 branch = GetCurrentBranch()
230
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000231 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700232 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000233
tandrii5d48c322016-08-18 16:19:37 -0700234 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700235 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700236 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700237 # git config also has --int, but apparently git config suffers from integer
238 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700239 args.append(_git_branch_config_key(branch, key))
240 code, out = RunGitWithCode(args)
241 if code == 0:
242 value = out.strip()
243 if value_type == int:
244 return int(value)
245 if value_type == bool:
246 return bool(value.lower() == 'true')
247 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000248 return default
249
250
tandrii5d48c322016-08-18 16:19:37 -0700251def _git_set_branch_config_value(key, value, branch=None, **kwargs):
252 """Sets the value or unsets if it's None of a git branch config.
253
254 Valid, though not necessarily existing, branch must be provided,
255 otherwise currently checked out branch is used.
256 """
257 if not branch:
258 branch = GetCurrentBranch()
259 assert branch, 'a branch name OR currently checked out branch is required'
260 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700261 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700262 if value is None:
263 args.append('--unset')
264 elif isinstance(value, bool):
265 args.append('--bool')
266 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700267 else:
tandrii33a46ff2016-08-23 05:53:40 -0700268 # git config also has --int, but apparently git config suffers from integer
269 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700270 value = str(value)
271 args.append(_git_branch_config_key(branch, key))
272 if value is not None:
273 args.append(value)
274 RunGit(args, **kwargs)
275
276
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100277def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700278 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100279
280 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
281 """
282 # Git also stores timezone offset, but it only affects visual display,
283 # actual point in time is defined by this timestamp only.
284 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
285
286
287def _git_amend_head(message, committer_timestamp):
288 """Amends commit with new message and desired committer_timestamp.
289
290 Sets committer timezone to UTC.
291 """
292 env = os.environ.copy()
293 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
294 return RunGit(['commit', '--amend', '-m', message], env=env)
295
296
machenbach@chromium.org45453142015-09-15 08:45:22 +0000297def _get_properties_from_options(options):
298 properties = dict(x.split('=', 1) for x in options.properties)
299 for key, val in properties.iteritems():
300 try:
301 properties[key] = json.loads(val)
302 except ValueError:
303 pass # If a value couldn't be evaluated, treat it as a string.
304 return properties
305
306
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307def _prefix_master(master):
308 """Convert user-specified master name to full master name.
309
310 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
311 name, while the developers always use shortened master name
312 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
313 function does the conversion for buildbucket migration.
314 """
borenet6c0efe62016-10-19 08:13:29 -0700315 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000316 return master
borenet6c0efe62016-10-19 08:13:29 -0700317 return '%s%s' % (MASTER_PREFIX, master)
318
319
320def _unprefix_master(bucket):
321 """Convert bucket name to shortened master name.
322
323 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
324 name, while the developers always use shortened master name
325 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
326 function does the conversion for buildbucket migration.
327 """
328 if bucket.startswith(MASTER_PREFIX):
329 return bucket[len(MASTER_PREFIX):]
330 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000333def _buildbucket_retry(operation_name, http, *args, **kwargs):
334 """Retries requests to buildbucket service and returns parsed json content."""
335 try_count = 0
336 while True:
337 response, content = http.request(*args, **kwargs)
338 try:
339 content_json = json.loads(content)
340 except ValueError:
341 content_json = None
342
343 # Buildbucket could return an error even if status==200.
344 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000345 error = content_json.get('error')
346 if error.get('code') == 403:
347 raise BuildbucketResponseException(
348 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000349 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000350 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000351 raise BuildbucketResponseException(msg)
352
353 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700354 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000355 raise BuildbucketResponseException(
356 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700357 'Please file bugs at http://crbug.com, '
358 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 content)
360 return content_json
361 if response.status < 500 or try_count >= 2:
362 raise httplib2.HttpLib2Error(content)
363
364 # status >= 500 means transient failures.
365 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700366 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000367 try_count += 1
368 assert False, 'unreachable'
369
370
qyearsley1fdfcb62016-10-24 13:22:03 -0700371def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700372 """Returns a dict mapping bucket names to builders and tests,
373 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700374 """
qyearsleydd49f942016-10-28 11:57:22 -0700375 # If no bots are listed, we try to get a set of builders and tests based
376 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700377 if not options.bot:
378 change = changelist.GetChange(
379 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700380 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700381 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700382 change=change,
383 changed_files=change.LocalPaths(),
384 repository_root=settings.GetRoot(),
385 default_presubmit=None,
386 project=None,
387 verbose=options.verbose,
388 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700389 if masters is None:
390 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100391 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700392
qyearsley1fdfcb62016-10-24 13:22:03 -0700393 if options.bucket:
394 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700395 if options.master:
396 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700397
qyearsleydd49f942016-10-28 11:57:22 -0700398 # If bots are listed but no master or bucket, then we need to find out
399 # the corresponding master for each bot.
400 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
401 if error_message:
402 option_parser.error(
403 'Tryserver master cannot be found because: %s\n'
404 'Please manually specify the tryserver master, e.g. '
405 '"-m tryserver.chromium.linux".' % error_message)
406 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700407
408
qyearsley123a4682016-10-26 09:12:17 -0700409def _get_bucket_map_for_builders(builders):
410 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700411 map_url = 'https://builders-map.appspot.com/'
412 try:
qyearsley123a4682016-10-26 09:12:17 -0700413 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700414 except urllib2.URLError as e:
415 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
416 (map_url, e))
417 except ValueError as e:
418 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700419 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700420 return None, 'Failed to build master map.'
421
qyearsley123a4682016-10-26 09:12:17 -0700422 bucket_map = {}
423 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800424 bucket = builders_map.get(builder, {}).get('bucket')
425 if bucket:
426 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700427 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700428
429
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800430def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700431 """Sends a request to Buildbucket to trigger try jobs for a changelist.
432
433 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700434 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700435 changelist: Changelist that the try jobs are associated with.
436 buckets: A nested dict mapping bucket names to builders to tests.
437 options: Command-line options.
438 """
tandriide281ae2016-10-12 06:02:30 -0700439 assert changelist.GetIssue(), 'CL must be uploaded first'
440 codereview_url = changelist.GetCodereviewServer()
441 assert codereview_url, 'CL must be uploaded first'
442 patchset = patchset or changelist.GetMostRecentPatchset()
443 assert patchset, 'CL must be uploaded first'
444
445 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700446 # Cache the buildbucket credentials under the codereview host key, so that
447 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700448 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000449 http = authenticator.authorize(httplib2.Http())
450 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700451
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000452 buildbucket_put_url = (
453 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000454 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700455 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
456 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
457 hostname=codereview_host,
458 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700460
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700461 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800462 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700463 if options.clobber:
464 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700465 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700466 if extra_properties:
467 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000468
469 batch_req_body = {'builds': []}
470 print_text = []
471 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700472 for bucket, builders_and_tests in sorted(buckets.iteritems()):
473 print_text.append('Bucket: %s' % bucket)
474 master = None
475 if bucket.startswith(MASTER_PREFIX):
476 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000477 for builder, tests in sorted(builders_and_tests.iteritems()):
478 print_text.append(' %s: %s' % (builder, tests))
479 parameters = {
480 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000481 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100482 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000483 'revision': options.revision,
484 }],
tandrii8c5a3532016-11-04 07:52:02 -0700485 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000486 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000487 if 'presubmit' in builder.lower():
488 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000489 if tests:
490 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700491
492 tags = [
493 'builder:%s' % builder,
494 'buildset:%s' % buildset,
495 'user_agent:git_cl_try',
496 ]
497 if master:
498 parameters['properties']['master'] = master
499 tags.append('master:%s' % master)
500
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000501 batch_req_body['builds'].append(
502 {
503 'bucket': bucket,
504 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000505 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700506 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000507 }
508 )
509
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700511 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000512 http,
513 buildbucket_put_url,
514 'PUT',
515 body=json.dumps(batch_req_body),
516 headers={'Content-Type': 'application/json'}
517 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000518 print_text.append('To see results here, run: git cl try-results')
519 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700520 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000521
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000522
tandrii221ab252016-10-06 08:12:04 -0700523def fetch_try_jobs(auth_config, changelist, buildbucket_host,
524 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700525 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000526
qyearsley53f48a12016-09-01 10:45:13 -0700527 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000528 """
tandrii221ab252016-10-06 08:12:04 -0700529 assert buildbucket_host
530 assert changelist.GetIssue(), 'CL must be uploaded first'
531 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
532 patchset = patchset or changelist.GetMostRecentPatchset()
533 assert patchset, 'CL must be uploaded first'
534
535 codereview_url = changelist.GetCodereviewServer()
536 codereview_host = urlparse.urlparse(codereview_url).hostname
537 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000538 if authenticator.has_cached_credentials():
539 http = authenticator.authorize(httplib2.Http())
540 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700541 print('Warning: Some results might be missing because %s' %
542 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700543 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 http = httplib2.Http()
545
546 http.force_exception_to_status_code = True
547
tandrii221ab252016-10-06 08:12:04 -0700548 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
549 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
550 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700552 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 params = {'tag': 'buildset:%s' % buildset}
554
555 builds = {}
556 while True:
557 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700558 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700560 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000561 for build in content.get('builds', []):
562 builds[build['id']] = build
563 if 'next_cursor' in content:
564 params['start_cursor'] = content['next_cursor']
565 else:
566 break
567 return builds
568
569
qyearsleyeab3c042016-08-24 09:18:28 -0700570def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000571 """Prints nicely result of fetch_try_jobs."""
572 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700573 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000574 return
575
576 # Make a copy, because we'll be modifying builds dictionary.
577 builds = builds.copy()
578 builder_names_cache = {}
579
580 def get_builder(b):
581 try:
582 return builder_names_cache[b['id']]
583 except KeyError:
584 try:
585 parameters = json.loads(b['parameters_json'])
586 name = parameters['builder_name']
587 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700588 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700589 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000590 name = None
591 builder_names_cache[b['id']] = name
592 return name
593
594 def get_bucket(b):
595 bucket = b['bucket']
596 if bucket.startswith('master.'):
597 return bucket[len('master.'):]
598 return bucket
599
600 if options.print_master:
601 name_fmt = '%%-%ds %%-%ds' % (
602 max(len(str(get_bucket(b))) for b in builds.itervalues()),
603 max(len(str(get_builder(b))) for b in builds.itervalues()))
604 def get_name(b):
605 return name_fmt % (get_bucket(b), get_builder(b))
606 else:
607 name_fmt = '%%-%ds' % (
608 max(len(str(get_builder(b))) for b in builds.itervalues()))
609 def get_name(b):
610 return name_fmt % get_builder(b)
611
612 def sort_key(b):
613 return b['status'], b.get('result'), get_name(b), b.get('url')
614
615 def pop(title, f, color=None, **kwargs):
616 """Pop matching builds from `builds` dict and print them."""
617
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000618 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000619 colorize = str
620 else:
621 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
622
623 result = []
624 for b in builds.values():
625 if all(b.get(k) == v for k, v in kwargs.iteritems()):
626 builds.pop(b['id'])
627 result.append(b)
628 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700629 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000630 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700631 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000632
633 total = len(builds)
634 pop(status='COMPLETED', result='SUCCESS',
635 title='Successes:', color=Fore.GREEN,
636 f=lambda b: (get_name(b), b.get('url')))
637 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
638 title='Infra Failures:', color=Fore.MAGENTA,
639 f=lambda b: (get_name(b), b.get('url')))
640 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
641 title='Failures:', color=Fore.RED,
642 f=lambda b: (get_name(b), b.get('url')))
643 pop(status='COMPLETED', result='CANCELED',
644 title='Canceled:', color=Fore.MAGENTA,
645 f=lambda b: (get_name(b),))
646 pop(status='COMPLETED', result='FAILURE',
647 failure_reason='INVALID_BUILD_DEFINITION',
648 title='Wrong master/builder name:', color=Fore.MAGENTA,
649 f=lambda b: (get_name(b),))
650 pop(status='COMPLETED', result='FAILURE',
651 title='Other failures:',
652 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
653 pop(status='COMPLETED',
654 title='Other finished:',
655 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
656 pop(status='STARTED',
657 title='Started:', color=Fore.YELLOW,
658 f=lambda b: (get_name(b), b.get('url')))
659 pop(status='SCHEDULED',
660 title='Scheduled:',
661 f=lambda b: (get_name(b), 'id=%s' % b['id']))
662 # The last section is just in case buildbucket API changes OR there is a bug.
663 pop(title='Other:',
664 f=lambda b: (get_name(b), 'id=%s' % b['id']))
665 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700666 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000667
668
qyearsley53f48a12016-09-01 10:45:13 -0700669def write_try_results_json(output_file, builds):
670 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
671
672 The input |builds| dict is assumed to be generated by Buildbucket.
673 Buildbucket documentation: http://goo.gl/G0s101
674 """
675
676 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800677 """Extracts some of the information from one build dict."""
678 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700679 return {
680 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700681 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800682 'builder_name': parameters.get('builder_name'),
683 'created_ts': build.get('created_ts'),
684 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700685 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800686 'result': build.get('result'),
687 'status': build.get('status'),
688 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700689 'url': build.get('url'),
690 }
691
692 converted = []
693 for _, build in sorted(builds.items()):
694 converted.append(convert_build_dict(build))
695 write_json(output_file, converted)
696
697
Aaron Gable13101a62018-02-09 13:20:41 -0800698def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000699 """Prints statistics about the change to the user."""
700 # --no-ext-diff is broken in some versions of Git, so try to work around
701 # this by overriding the environment (but there is still a problem if the
702 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000703 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000704 if 'GIT_EXTERNAL_DIFF' in env:
705 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000706
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000707 try:
708 stdout = sys.stdout.fileno()
709 except AttributeError:
710 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000711 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800712 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000713 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000714
715
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000716class BuildbucketResponseException(Exception):
717 pass
718
719
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720class Settings(object):
721 def __init__(self):
722 self.default_server = None
723 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000724 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 self.tree_status_url = None
726 self.viewvc_url = None
727 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000728 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000729 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000730 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000731 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000732 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000733 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734
735 def LazyUpdateIfNeeded(self):
736 """Updates the settings from a codereview.settings file, if available."""
737 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000738 # The only value that actually changes the behavior is
739 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000740 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000741 error_ok=True
742 ).strip().lower()
743
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000745 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746 LoadCodereviewSettingsFromFile(cr_settings_file)
747 self.updated = True
748
749 def GetDefaultServerUrl(self, error_ok=False):
750 if not self.default_server:
751 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000752 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000753 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000754 if error_ok:
755 return self.default_server
756 if not self.default_server:
757 error_message = ('Could not find settings file. You must configure '
758 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000759 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000760 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000761 return self.default_server
762
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000763 @staticmethod
764 def GetRelativeRoot():
765 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000766
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000768 if self.root is None:
769 self.root = os.path.abspath(self.GetRelativeRoot())
770 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000771
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000772 def GetGitMirror(self, remote='origin'):
773 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000774 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000775 if not os.path.isdir(local_url):
776 return None
777 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
778 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100779 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100780 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000781 if mirror.exists():
782 return mirror
783 return None
784
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 def GetTreeStatusUrl(self, error_ok=False):
786 if not self.tree_status_url:
787 error_message = ('You must configure your tree status URL by running '
788 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000789 self.tree_status_url = self._GetRietveldConfig(
790 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791 return self.tree_status_url
792
793 def GetViewVCUrl(self):
794 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000795 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 return self.viewvc_url
797
rmistry@google.com90752582014-01-14 21:04:50 +0000798 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000799 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000800
rmistry@google.com78948ed2015-07-08 23:09:57 +0000801 def GetIsSkipDependencyUpload(self, branch_name):
802 """Returns true if specified branch should skip dep uploads."""
803 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
804 error_ok=True)
805
rmistry@google.com5626a922015-02-26 14:03:30 +0000806 def GetRunPostUploadHook(self):
807 run_post_upload_hook = self._GetRietveldConfig(
808 'run-post-upload-hook', error_ok=True)
809 return run_post_upload_hook == "True"
810
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000811 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000812 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000813
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000814 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000815 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000816
ukai@chromium.orge8077812012-02-03 03:41:46 +0000817 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700818 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000819 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700820 self.is_gerrit = (
821 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000822 return self.is_gerrit
823
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000824 def GetSquashGerritUploads(self):
825 """Return true if uploads to Gerrit should be squashed by default."""
826 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700827 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
828 if self.squash_gerrit_uploads is None:
829 # Default is squash now (http://crbug.com/611892#c23).
830 self.squash_gerrit_uploads = not (
831 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
832 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000833 return self.squash_gerrit_uploads
834
tandriia60502f2016-06-20 02:01:53 -0700835 def GetSquashGerritUploadsOverride(self):
836 """Return True or False if codereview.settings should be overridden.
837
838 Returns None if no override has been defined.
839 """
840 # See also http://crbug.com/611892#c23
841 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
842 error_ok=True).strip()
843 if result == 'true':
844 return True
845 if result == 'false':
846 return False
847 return None
848
tandrii@chromium.org28253532016-04-14 13:46:56 +0000849 def GetGerritSkipEnsureAuthenticated(self):
850 """Return True if EnsureAuthenticated should not be done for Gerrit
851 uploads."""
852 if self.gerrit_skip_ensure_authenticated is None:
853 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000854 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000855 error_ok=True).strip() == 'true')
856 return self.gerrit_skip_ensure_authenticated
857
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000858 def GetGitEditor(self):
859 """Return the editor specified in the git config, or None if none is."""
860 if self.git_editor is None:
861 self.git_editor = self._GetConfig('core.editor', error_ok=True)
862 return self.git_editor or None
863
thestig@chromium.org44202a22014-03-11 19:22:18 +0000864 def GetLintRegex(self):
865 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
866 DEFAULT_LINT_REGEX)
867
868 def GetLintIgnoreRegex(self):
869 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
870 DEFAULT_LINT_IGNORE_REGEX)
871
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000872 def GetProject(self):
873 if not self.project:
874 self.project = self._GetRietveldConfig('project', error_ok=True)
875 return self.project
876
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000877 def _GetRietveldConfig(self, param, **kwargs):
878 return self._GetConfig('rietveld.' + param, **kwargs)
879
rmistry@google.com78948ed2015-07-08 23:09:57 +0000880 def _GetBranchConfig(self, branch_name, param, **kwargs):
881 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
882
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883 def _GetConfig(self, param, **kwargs):
884 self.LazyUpdateIfNeeded()
885 return RunGit(['config', param], **kwargs).strip()
886
887
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100888@contextlib.contextmanager
889def _get_gerrit_project_config_file(remote_url):
890 """Context manager to fetch and store Gerrit's project.config from
891 refs/meta/config branch and store it in temp file.
892
893 Provides a temporary filename or None if there was error.
894 """
895 error, _ = RunGitWithCode([
896 'fetch', remote_url,
897 '+refs/meta/config:refs/git_cl/meta/config'])
898 if error:
899 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700900 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100901 (remote_url, error))
902 yield None
903 return
904
905 error, project_config_data = RunGitWithCode(
906 ['show', 'refs/git_cl/meta/config:project.config'])
907 if error:
908 print('WARNING: project.config file not found')
909 yield None
910 return
911
912 with gclient_utils.temporary_directory() as tempdir:
913 project_config_file = os.path.join(tempdir, 'project.config')
914 gclient_utils.FileWrite(project_config_file, project_config_data)
915 yield project_config_file
916
917
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000918def ShortBranchName(branch):
919 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000920 return branch.replace('refs/heads/', '', 1)
921
922
923def GetCurrentBranchRef():
924 """Returns branch ref (e.g., refs/heads/master) or None."""
925 return RunGit(['symbolic-ref', 'HEAD'],
926 stderr=subprocess2.VOID, error_ok=True).strip() or None
927
928
929def GetCurrentBranch():
930 """Returns current branch or None.
931
932 For refs/heads/* branches, returns just last part. For others, full ref.
933 """
934 branchref = GetCurrentBranchRef()
935 if branchref:
936 return ShortBranchName(branchref)
937 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938
939
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000940class _CQState(object):
941 """Enum for states of CL with respect to Commit Queue."""
942 NONE = 'none'
943 DRY_RUN = 'dry_run'
944 COMMIT = 'commit'
945
946 ALL_STATES = [NONE, DRY_RUN, COMMIT]
947
948
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000949class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200950 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000951 self.issue = issue
952 self.patchset = patchset
953 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200954 assert codereview in (None, 'rietveld', 'gerrit')
955 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000956
957 @property
958 def valid(self):
959 return self.issue is not None
960
961
Andrii Shyshkalovc9712392017-04-11 13:35:21 +0200962def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000963 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
964 fail_result = _ParsedIssueNumberArgument()
965
966 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -0700967 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000968 if not arg.startswith('http'):
969 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -0700970
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000971 url = gclient_utils.UpgradeToHttps(arg)
972 try:
973 parsed_url = urlparse.urlparse(url)
974 except ValueError:
975 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200976
Andrii Shyshkalovc9712392017-04-11 13:35:21 +0200977 if codereview is not None:
978 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
979 return parsed or fail_result
980
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200981 results = {}
982 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
983 parsed = cls.ParseIssueURL(parsed_url)
984 if parsed is not None:
985 results[name] = parsed
986
987 if not results:
988 return fail_result
989 if len(results) == 1:
990 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +0200991
992 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
993 # This is likely Gerrit.
994 return results['gerrit']
995 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200996 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000997
998
Aaron Gablea45ee112016-11-22 15:14:38 -0800999class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001000 def __init__(self, issue, url):
1001 self.issue = issue
1002 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001003 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001004
1005 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001006 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001007 self.issue, self.url)
1008
1009
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001010_CommentSummary = collections.namedtuple(
1011 '_CommentSummary', ['date', 'message', 'sender',
1012 # TODO(tandrii): these two aren't known in Gerrit.
1013 'approval', 'disapproval'])
1014
1015
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001017 """Changelist works with one changelist in local branch.
1018
1019 Supports two codereview backends: Rietveld or Gerrit, selected at object
1020 creation.
1021
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001022 Notes:
1023 * Not safe for concurrent multi-{thread,process} use.
1024 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001025 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001026 """
1027
1028 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1029 """Create a new ChangeList instance.
1030
1031 If issue is given, the codereview must be given too.
1032
1033 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1034 Otherwise, it's decided based on current configuration of the local branch,
1035 with default being 'rietveld' for backwards compatibility.
1036 See _load_codereview_impl for more details.
1037
1038 **kwargs will be passed directly to codereview implementation.
1039 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001041 global settings
1042 if not settings:
1043 # Happens when git_cl.py is used as a utility library.
1044 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001045
1046 if issue:
1047 assert codereview, 'codereview must be known, if issue is known'
1048
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049 self.branchref = branchref
1050 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001051 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052 self.branch = ShortBranchName(self.branchref)
1053 else:
1054 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001056 self.lookedup_issue = False
1057 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058 self.has_description = False
1059 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001060 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001062 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001063 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001064 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001065 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001066
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001067 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001068 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001069 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001070 assert self._codereview_impl
1071 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001072
1073 def _load_codereview_impl(self, codereview=None, **kwargs):
1074 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001075 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1076 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1077 self._codereview = codereview
1078 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001079 return
1080
1081 # Automatic selection based on issue number set for a current branch.
1082 # Rietveld takes precedence over Gerrit.
1083 assert not self.issue
1084 # Whether we find issue or not, we are doing the lookup.
1085 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001086 if self.GetBranch():
1087 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1088 issue = _git_get_branch_config_value(
1089 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1090 if issue:
1091 self._codereview = codereview
1092 self._codereview_impl = cls(self, **kwargs)
1093 self.issue = int(issue)
1094 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001095
1096 # No issue is set for this branch, so decide based on repo-wide settings.
1097 return self._load_codereview_impl(
1098 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1099 **kwargs)
1100
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001101 def IsGerrit(self):
1102 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001103
1104 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001105 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001106
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001107 The return value is a string suitable for passing to git cl with the --cc
1108 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001109 """
1110 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001111 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001112 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001113 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1114 return self.cc
1115
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001116 def GetCCListWithoutDefault(self):
1117 """Return the users cc'd on this CL excluding default ones."""
1118 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001119 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001120 return self.cc
1121
Daniel Cheng7227d212017-11-17 08:12:37 -08001122 def ExtendCC(self, more_cc):
1123 """Extends the list of users to cc on this CL based on the changed files."""
1124 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125
1126 def GetBranch(self):
1127 """Returns the short branch name, e.g. 'master'."""
1128 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001129 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001130 if not branchref:
1131 return None
1132 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133 self.branch = ShortBranchName(self.branchref)
1134 return self.branch
1135
1136 def GetBranchRef(self):
1137 """Returns the full branch name, e.g. 'refs/heads/master'."""
1138 self.GetBranch() # Poke the lazy loader.
1139 return self.branchref
1140
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001141 def ClearBranch(self):
1142 """Clears cached branch data of this object."""
1143 self.branch = self.branchref = None
1144
tandrii5d48c322016-08-18 16:19:37 -07001145 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1146 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1147 kwargs['branch'] = self.GetBranch()
1148 return _git_get_branch_config_value(key, default, **kwargs)
1149
1150 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1151 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1152 assert self.GetBranch(), (
1153 'this CL must have an associated branch to %sset %s%s' %
1154 ('un' if value is None else '',
1155 key,
1156 '' if value is None else ' to %r' % value))
1157 kwargs['branch'] = self.GetBranch()
1158 return _git_set_branch_config_value(key, value, **kwargs)
1159
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001160 @staticmethod
1161 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001162 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163 e.g. 'origin', 'refs/heads/master'
1164 """
1165 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001166 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1167
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001169 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001171 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1172 error_ok=True).strip()
1173 if upstream_branch:
1174 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001176 # Else, try to guess the origin remote.
1177 remote_branches = RunGit(['branch', '-r']).split()
1178 if 'origin/master' in remote_branches:
1179 # Fall back on origin/master if it exits.
1180 remote = 'origin'
1181 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001183 DieWithError(
1184 'Unable to determine default branch to diff against.\n'
1185 'Either pass complete "git diff"-style arguments, like\n'
1186 ' git cl upload origin/master\n'
1187 'or verify this branch is set up to track another \n'
1188 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189
1190 return remote, upstream_branch
1191
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001192 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001193 upstream_branch = self.GetUpstreamBranch()
1194 if not BranchExists(upstream_branch):
1195 DieWithError('The upstream for the current branch (%s) does not exist '
1196 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001197 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001198 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001199
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001200 def GetUpstreamBranch(self):
1201 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001202 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001204 upstream_branch = upstream_branch.replace('refs/heads/',
1205 'refs/remotes/%s/' % remote)
1206 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1207 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 self.upstream_branch = upstream_branch
1209 return self.upstream_branch
1210
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001211 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001212 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001213 remote, branch = None, self.GetBranch()
1214 seen_branches = set()
1215 while branch not in seen_branches:
1216 seen_branches.add(branch)
1217 remote, branch = self.FetchUpstreamTuple(branch)
1218 branch = ShortBranchName(branch)
1219 if remote != '.' or branch.startswith('refs/remotes'):
1220 break
1221 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001222 remotes = RunGit(['remote'], error_ok=True).split()
1223 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001224 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001225 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001226 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001227 logging.warn('Could not determine which remote this change is '
1228 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001229 else:
1230 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001231 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001232 branch = 'HEAD'
1233 if branch.startswith('refs/remotes'):
1234 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001235 elif branch.startswith('refs/branch-heads/'):
1236 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001237 else:
1238 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001239 return self._remote
1240
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001241 def GitSanityChecks(self, upstream_git_obj):
1242 """Checks git repo status and ensures diff is from local commits."""
1243
sbc@chromium.org79706062015-01-14 21:18:12 +00001244 if upstream_git_obj is None:
1245 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001246 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001247 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001248 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001249 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001250 return False
1251
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001252 # Verify the commit we're diffing against is in our current branch.
1253 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1254 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1255 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001256 print('ERROR: %s is not in the current branch. You may need to rebase '
1257 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001258 return False
1259
1260 # List the commits inside the diff, and verify they are all local.
1261 commits_in_diff = RunGit(
1262 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1263 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1264 remote_branch = remote_branch.strip()
1265 if code != 0:
1266 _, remote_branch = self.GetRemoteBranch()
1267
1268 commits_in_remote = RunGit(
1269 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1270
1271 common_commits = set(commits_in_diff) & set(commits_in_remote)
1272 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001273 print('ERROR: Your diff contains %d commits already in %s.\n'
1274 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1275 'the diff. If you are using a custom git flow, you can override'
1276 ' the reference used for this check with "git config '
1277 'gitcl.remotebranch <git-ref>".' % (
1278 len(common_commits), remote_branch, upstream_git_obj),
1279 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001280 return False
1281 return True
1282
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001283 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001284 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001285
1286 Returns None if it is not set.
1287 """
tandrii5d48c322016-08-18 16:19:37 -07001288 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290 def GetRemoteUrl(self):
1291 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1292
1293 Returns None if there is no remote.
1294 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001295 is_cached, value = self._cached_remote_url
1296 if is_cached:
1297 return value
1298
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001299 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001300 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1301
1302 # If URL is pointing to a local directory, it is probably a git cache.
1303 if os.path.isdir(url):
1304 url = RunGit(['config', 'remote.%s.url' % remote],
1305 error_ok=True,
1306 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001307 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001308 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001309
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001310 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001311 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001312 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001313 self.issue = self._GitGetBranchConfigValue(
1314 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001315 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316 return self.issue
1317
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318 def GetIssueURL(self):
1319 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001320 issue = self.GetIssue()
1321 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001322 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001323 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001325 def GetDescription(self, pretty=False, force=False):
1326 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001328 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329 self.has_description = True
1330 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001331 # Set width to 72 columns + 2 space indent.
1332 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001333 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001334 lines = self.description.splitlines()
1335 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336 return self.description
1337
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001338 def GetDescriptionFooters(self):
1339 """Returns (non_footer_lines, footers) for the commit message.
1340
1341 Returns:
1342 non_footer_lines (list(str)) - Simple list of description lines without
1343 any footer. The lines do not contain newlines, nor does the list contain
1344 the empty line between the message and the footers.
1345 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1346 [("Change-Id", "Ideadbeef...."), ...]
1347 """
1348 raw_description = self.GetDescription()
1349 msg_lines, _, footers = git_footers.split_footers(raw_description)
1350 if footers:
1351 msg_lines = msg_lines[:len(msg_lines)-1]
1352 return msg_lines, footers
1353
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001355 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001356 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001357 self.patchset = self._GitGetBranchConfigValue(
1358 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001359 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 return self.patchset
1361
1362 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001363 """Set this branch's patchset. If patchset=0, clears the patchset."""
1364 assert self.GetBranch()
1365 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001366 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001367 else:
1368 self.patchset = int(patchset)
1369 self._GitSetBranchConfigValue(
1370 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001372 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001373 """Set this branch's issue. If issue isn't given, clears the issue."""
1374 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001376 issue = int(issue)
1377 self._GitSetBranchConfigValue(
1378 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001379 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001380 codereview_server = self._codereview_impl.GetCodereviewServer()
1381 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001382 self._GitSetBranchConfigValue(
1383 self._codereview_impl.CodereviewServerConfigKey(),
1384 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385 else:
tandrii5d48c322016-08-18 16:19:37 -07001386 # Reset all of these just to be clean.
1387 reset_suffixes = [
1388 'last-upload-hash',
1389 self._codereview_impl.IssueConfigKey(),
1390 self._codereview_impl.PatchsetConfigKey(),
1391 self._codereview_impl.CodereviewServerConfigKey(),
1392 ] + self._PostUnsetIssueProperties()
1393 for prop in reset_suffixes:
1394 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001395 msg = RunGit(['log', '-1', '--format=%B']).strip()
1396 if msg and git_footers.get_footer_change_id(msg):
1397 print('WARNING: The change patched into this branch has a Change-Id. '
1398 'Removing it.')
1399 RunGit(['commit', '--amend', '-m',
1400 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001401 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001402 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403
dnjba1b0f32016-09-02 12:37:42 -07001404 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001405 if not self.GitSanityChecks(upstream_branch):
1406 DieWithError('\nGit sanity check failure')
1407
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001408 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001409 if not root:
1410 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001411 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001412
1413 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001414 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001415 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001416 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001417 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001418 except subprocess2.CalledProcessError:
1419 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001420 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001421 'This branch probably doesn\'t exist anymore. To reset the\n'
1422 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001423 ' git branch --set-upstream-to origin/master %s\n'
1424 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001425 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001426
maruel@chromium.org52424302012-08-29 15:14:30 +00001427 issue = self.GetIssue()
1428 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001429 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001430 description = self.GetDescription()
1431 else:
1432 # If the change was never uploaded, use the log messages of all commits
1433 # up to the branch point, as git cl upload will prefill the description
1434 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001435 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1436 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001437
1438 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001439 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001440 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001441 name,
1442 description,
1443 absroot,
1444 files,
1445 issue,
1446 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001447 author,
1448 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001449
dsansomee2d6fd92016-09-08 00:10:47 -07001450 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001451 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001452 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001453 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001454
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001455 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1456 """Sets the description for this CL remotely.
1457
1458 You can get description_lines and footers with GetDescriptionFooters.
1459
1460 Args:
1461 description_lines (list(str)) - List of CL description lines without
1462 newline characters.
1463 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1464 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1465 `List-Of-Tokens`). It will be case-normalized so that each token is
1466 title-cased.
1467 """
1468 new_description = '\n'.join(description_lines)
1469 if footers:
1470 new_description += '\n'
1471 for k, v in footers:
1472 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1473 if not git_footers.FOOTER_PATTERN.match(foot):
1474 raise ValueError('Invalid footer %r' % foot)
1475 new_description += foot + '\n'
1476 self.UpdateDescription(new_description, force)
1477
Edward Lesmes8e282792018-04-03 18:50:29 -04001478 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001479 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1480 try:
1481 return presubmit_support.DoPresubmitChecks(change, committing,
1482 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1483 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001484 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1485 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001486 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001487 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001488
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001489 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1490 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001491 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1492 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001493 else:
1494 # Assume url.
1495 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1496 urlparse.urlparse(issue_arg))
1497 if not parsed_issue_arg or not parsed_issue_arg.valid:
1498 DieWithError('Failed to parse issue argument "%s". '
1499 'Must be an issue number or a valid URL.' % issue_arg)
1500 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001501 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001502
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001503 def CMDUpload(self, options, git_diff_args, orig_args):
1504 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001505 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001506 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001507 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001508 else:
1509 if self.GetBranch() is None:
1510 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1511
1512 # Default to diffing against common ancestor of upstream branch
1513 base_branch = self.GetCommonAncestorWithUpstream()
1514 git_diff_args = [base_branch, 'HEAD']
1515
Aaron Gablec4c40d12017-05-22 11:49:53 -07001516 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1517 if not self.IsGerrit() and not self.GetIssue():
1518 print('=====================================')
1519 print('NOTICE: Rietveld is being deprecated. '
1520 'You can upload changes to Gerrit with')
1521 print(' git cl upload --gerrit')
1522 print('or set Gerrit to be your default code review tool with')
1523 print(' git config gerrit.host true')
1524 print('=====================================')
1525
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001526 # Fast best-effort checks to abort before running potentially
1527 # expensive hooks if uploading is likely to fail anyway. Passing these
1528 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001529 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001530 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001531
1532 # Apply watchlists on upload.
1533 change = self.GetChange(base_branch, None)
1534 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1535 files = [f.LocalPath() for f in change.AffectedFiles()]
1536 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001537 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001538
1539 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001540 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001541 # Set the reviewer list now so that presubmit checks can access it.
1542 change_description = ChangeDescription(change.FullDescriptionText())
1543 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001544 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001545 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001546 change)
1547 change.SetDescriptionText(change_description.description)
1548 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001549 may_prompt=not options.force,
1550 verbose=options.verbose,
1551 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001552 if not hook_results.should_continue():
1553 return 1
1554 if not options.reviewers and hook_results.reviewers:
1555 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001556 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001557
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001558 # TODO(tandrii): Checking local patchset against remote patchset is only
1559 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1560 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001561 latest_patchset = self.GetMostRecentPatchset()
1562 local_patchset = self.GetPatchset()
1563 if (latest_patchset and local_patchset and
1564 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001565 print('The last upload made from this repository was patchset #%d but '
1566 'the most recent patchset on the server is #%d.'
1567 % (local_patchset, latest_patchset))
1568 print('Uploading will still work, but if you\'ve uploaded to this '
1569 'issue from another machine or branch the patch you\'re '
1570 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001571 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001572
Aaron Gable13101a62018-02-09 13:20:41 -08001573 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001574 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001575 if not ret:
Ravi Mistry31e7d562018-04-02 12:53:57 -04001576 if self.IsGerrit():
1577 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
1578 options.cq_dry_run);
1579 else:
1580 if options.use_commit_queue:
1581 self.SetCQState(_CQState.COMMIT)
1582 elif options.cq_dry_run:
1583 self.SetCQState(_CQState.DRY_RUN)
tandrii4d0545a2016-07-06 03:56:49 -07001584
tandrii5d48c322016-08-18 16:19:37 -07001585 _git_set_branch_config_value('last-upload-hash',
1586 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001587 # Run post upload hooks, if specified.
1588 if settings.GetRunPostUploadHook():
1589 presubmit_support.DoPostUploadExecuter(
1590 change,
1591 self,
1592 settings.GetRoot(),
1593 options.verbose,
1594 sys.stdout)
1595
1596 # Upload all dependencies if specified.
1597 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001598 print()
1599 print('--dependencies has been specified.')
1600 print('All dependent local branches will be re-uploaded.')
1601 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001602 # Remove the dependencies flag from args so that we do not end up in a
1603 # loop.
1604 orig_args.remove('--dependencies')
1605 ret = upload_branch_deps(self, orig_args)
1606 return ret
1607
Ravi Mistry31e7d562018-04-02 12:53:57 -04001608 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1609 """Sets labels on the change based on the provided flags.
1610
1611 Sets labels if issue is already uploaded and known, else returns without
1612 doing anything.
1613
1614 Args:
1615 enable_auto_submit: Sets Auto-Submit+1 on the change.
1616 use_commit_queue: Sets Commit-Queue+2 on the change.
1617 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1618 both use_commit_queue and cq_dry_run are true.
1619 """
1620 if not self.GetIssue():
1621 return
1622 try:
1623 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1624 cq_dry_run)
1625 return 0
1626 except KeyboardInterrupt:
1627 raise
1628 except:
1629 labels = []
1630 if enable_auto_submit:
1631 labels.append('Auto-Submit')
1632 if use_commit_queue or cq_dry_run:
1633 labels.append('Commit-Queue')
1634 print('WARNING: Failed to set label(s) on your change: %s\n'
1635 'Either:\n'
1636 ' * Your project does not have the above label(s),\n'
1637 ' * You don\'t have permission to set the above label(s),\n'
1638 ' * There\'s a bug in this code (see stack trace below).\n' %
1639 (', '.join(labels)))
1640 # Still raise exception so that stack trace is printed.
1641 raise
1642
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001643 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001644 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001645
1646 Issue must have been already uploaded and known.
1647 """
1648 assert new_state in _CQState.ALL_STATES
1649 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001650 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001651 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001652 return 0
1653 except KeyboardInterrupt:
1654 raise
1655 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001656 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001657 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001658 ' * Your project has no CQ,\n'
1659 ' * You don\'t have permission to change the CQ state,\n'
1660 ' * There\'s a bug in this code (see stack trace below).\n'
1661 'Consider specifying which bots to trigger manually or asking your '
1662 'project owners for permissions or contacting Chrome Infra at:\n'
1663 'https://www.chromium.org/infra\n\n' %
1664 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001665 # Still raise exception so that stack trace is printed.
1666 raise
1667
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001668 # Forward methods to codereview specific implementation.
1669
Aaron Gable636b13f2017-07-14 10:42:48 -07001670 def AddComment(self, message, publish=None):
1671 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001672
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001673 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001674 """Returns list of _CommentSummary for each comment.
1675
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001676 args:
1677 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001678 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001679 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001680
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001681 def CloseIssue(self):
1682 return self._codereview_impl.CloseIssue()
1683
1684 def GetStatus(self):
1685 return self._codereview_impl.GetStatus()
1686
1687 def GetCodereviewServer(self):
1688 return self._codereview_impl.GetCodereviewServer()
1689
tandriide281ae2016-10-12 06:02:30 -07001690 def GetIssueOwner(self):
1691 """Get owner from codereview, which may differ from this checkout."""
1692 return self._codereview_impl.GetIssueOwner()
1693
Edward Lemur707d70b2018-02-07 00:50:14 +01001694 def GetReviewers(self):
1695 return self._codereview_impl.GetReviewers()
1696
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001697 def GetMostRecentPatchset(self):
1698 return self._codereview_impl.GetMostRecentPatchset()
1699
tandriide281ae2016-10-12 06:02:30 -07001700 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001701 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001702 return self._codereview_impl.CannotTriggerTryJobReason()
1703
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001704 def GetTryJobProperties(self, patchset=None):
1705 """Returns dictionary of properties to launch try job."""
1706 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001707
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001708 def __getattr__(self, attr):
1709 # This is because lots of untested code accesses Rietveld-specific stuff
1710 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001711 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001712 # Note that child method defines __getattr__ as well, and forwards it here,
1713 # because _RietveldChangelistImpl is not cleaned up yet, and given
1714 # deprecation of Rietveld, it should probably be just removed.
1715 # Until that time, avoid infinite recursion by bypassing __getattr__
1716 # of implementation class.
1717 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001718
1719
1720class _ChangelistCodereviewBase(object):
1721 """Abstract base class encapsulating codereview specifics of a changelist."""
1722 def __init__(self, changelist):
1723 self._changelist = changelist # instance of Changelist
1724
1725 def __getattr__(self, attr):
1726 # Forward methods to changelist.
1727 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1728 # _RietveldChangelistImpl to avoid this hack?
1729 return getattr(self._changelist, attr)
1730
1731 def GetStatus(self):
1732 """Apply a rough heuristic to give a simple summary of an issue's review
1733 or CQ status, assuming adherence to a common workflow.
1734
1735 Returns None if no issue for this branch, or specific string keywords.
1736 """
1737 raise NotImplementedError()
1738
1739 def GetCodereviewServer(self):
1740 """Returns server URL without end slash, like "https://codereview.com"."""
1741 raise NotImplementedError()
1742
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001743 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001744 """Fetches and returns description from the codereview server."""
1745 raise NotImplementedError()
1746
tandrii5d48c322016-08-18 16:19:37 -07001747 @classmethod
1748 def IssueConfigKey(cls):
1749 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001750 raise NotImplementedError()
1751
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001752 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001753 def PatchsetConfigKey(cls):
1754 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001755 raise NotImplementedError()
1756
tandrii5d48c322016-08-18 16:19:37 -07001757 @classmethod
1758 def CodereviewServerConfigKey(cls):
1759 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001760 raise NotImplementedError()
1761
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001762 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001763 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001764 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001765
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001766 def GetGerritObjForPresubmit(self):
1767 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1768 return None
1769
dsansomee2d6fd92016-09-08 00:10:47 -07001770 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001771 """Update the description on codereview site."""
1772 raise NotImplementedError()
1773
Aaron Gable636b13f2017-07-14 10:42:48 -07001774 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001775 """Posts a comment to the codereview site."""
1776 raise NotImplementedError()
1777
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001778 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001779 raise NotImplementedError()
1780
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001781 def CloseIssue(self):
1782 """Closes the issue."""
1783 raise NotImplementedError()
1784
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001785 def GetMostRecentPatchset(self):
1786 """Returns the most recent patchset number from the codereview site."""
1787 raise NotImplementedError()
1788
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001789 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001790 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001791 """Fetches and applies the issue.
1792
1793 Arguments:
1794 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1795 reject: if True, reject the failed patch instead of switching to 3-way
1796 merge. Rietveld only.
1797 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1798 only.
1799 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001800 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001801 """
1802 raise NotImplementedError()
1803
1804 @staticmethod
1805 def ParseIssueURL(parsed_url):
1806 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1807 failed."""
1808 raise NotImplementedError()
1809
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001810 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001811 """Best effort check that user is authenticated with codereview server.
1812
1813 Arguments:
1814 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001815 refresh: whether to attempt to refresh credentials. Ignored if not
1816 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001817 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001818 raise NotImplementedError()
1819
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001820 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001821 """Best effort check that uploading isn't supposed to fail for predictable
1822 reasons.
1823
1824 This method should raise informative exception if uploading shouldn't
1825 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001826
1827 Arguments:
1828 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001829 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001830 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001831
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001832 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001833 """Uploads a change to codereview."""
1834 raise NotImplementedError()
1835
Ravi Mistry31e7d562018-04-02 12:53:57 -04001836 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1837 """Sets labels on the change based on the provided flags.
1838
1839 Issue must have been already uploaded and known.
1840 """
1841 raise NotImplementedError()
1842
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001843 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001844 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001845
1846 Issue must have been already uploaded and known.
1847 """
1848 raise NotImplementedError()
1849
tandriie113dfd2016-10-11 10:20:12 -07001850 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001851 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001852 raise NotImplementedError()
1853
tandriide281ae2016-10-12 06:02:30 -07001854 def GetIssueOwner(self):
1855 raise NotImplementedError()
1856
Edward Lemur707d70b2018-02-07 00:50:14 +01001857 def GetReviewers(self):
1858 raise NotImplementedError()
1859
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001860 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001861 raise NotImplementedError()
1862
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001863
1864class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001865
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001866 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001867 super(_RietveldChangelistImpl, self).__init__(changelist)
1868 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001869 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001870 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001871
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001872 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001873 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001874 self._props = None
1875 self._rpc_server = None
1876
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001877 def GetCodereviewServer(self):
1878 if not self._rietveld_server:
1879 # If we're on a branch then get the server potentially associated
1880 # with that branch.
1881 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001882 self._rietveld_server = gclient_utils.UpgradeToHttps(
1883 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001884 if not self._rietveld_server:
1885 self._rietveld_server = settings.GetDefaultServerUrl()
1886 return self._rietveld_server
1887
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001888 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001889 """Best effort check that user is authenticated with Rietveld server."""
1890 if self._auth_config.use_oauth2:
1891 authenticator = auth.get_authenticator_for_host(
1892 self.GetCodereviewServer(), self._auth_config)
1893 if not authenticator.has_cached_credentials():
1894 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001895 if refresh:
1896 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001897
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001898 def EnsureCanUploadPatchset(self, force):
1899 # No checks for Rietveld because we are deprecating Rietveld.
1900 pass
1901
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001902 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001903 issue = self.GetIssue()
1904 assert issue
1905 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001906 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001907 except urllib2.HTTPError as e:
1908 if e.code == 404:
1909 DieWithError(
1910 ('\nWhile fetching the description for issue %d, received a '
1911 '404 (not found)\n'
1912 'error. It is likely that you deleted this '
1913 'issue on the server. If this is the\n'
1914 'case, please run\n\n'
1915 ' git cl issue 0\n\n'
1916 'to clear the association with the deleted issue. Then run '
1917 'this command again.') % issue)
1918 else:
1919 DieWithError(
1920 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1921 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001922 print('Warning: Failed to retrieve CL description due to network '
1923 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001924 return ''
1925
1926 def GetMostRecentPatchset(self):
1927 return self.GetIssueProperties()['patchsets'][-1]
1928
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001929 def GetIssueProperties(self):
1930 if self._props is None:
1931 issue = self.GetIssue()
1932 if not issue:
1933 self._props = {}
1934 else:
1935 self._props = self.RpcServer().get_issue_properties(issue, True)
1936 return self._props
1937
tandriie113dfd2016-10-11 10:20:12 -07001938 def CannotTriggerTryJobReason(self):
1939 props = self.GetIssueProperties()
1940 if not props:
1941 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1942 if props.get('closed'):
1943 return 'CL %s is closed' % self.GetIssue()
1944 if props.get('private'):
1945 return 'CL %s is private' % self.GetIssue()
1946 return None
1947
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001948 def GetTryJobProperties(self, patchset=None):
1949 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07001950 project = (self.GetIssueProperties() or {}).get('project')
1951 return {
1952 'issue': self.GetIssue(),
1953 'patch_project': project,
1954 'patch_storage': 'rietveld',
1955 'patchset': patchset or self.GetPatchset(),
1956 'rietveld': self.GetCodereviewServer(),
1957 }
1958
tandriide281ae2016-10-12 06:02:30 -07001959 def GetIssueOwner(self):
1960 return (self.GetIssueProperties() or {}).get('owner_email')
1961
Edward Lemur707d70b2018-02-07 00:50:14 +01001962 def GetReviewers(self):
1963 return (self.GetIssueProperties() or {}).get('reviewers')
1964
Aaron Gable636b13f2017-07-14 10:42:48 -07001965 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001966 return self.RpcServer().add_comment(self.GetIssue(), message)
1967
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001968 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001969 summary = []
1970 for message in self.GetIssueProperties().get('messages', []):
1971 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
1972 summary.append(_CommentSummary(
1973 date=date,
1974 disapproval=bool(message['disapproval']),
1975 approval=bool(message['approval']),
1976 sender=message['sender'],
1977 message=message['text'],
1978 ))
1979 return summary
1980
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001981 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001982 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001983 or CQ status, assuming adherence to a common workflow.
1984
1985 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07001986 * 'error' - error from review tool (including deleted issues)
1987 * 'unsent' - not sent for review
1988 * 'waiting' - waiting for review
1989 * 'reply' - waiting for owner to reply to review
1990 * 'not lgtm' - Code-Review label has been set negatively
1991 * 'lgtm' - LGTM from at least one approved reviewer
1992 * 'commit' - in the commit queue
1993 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001994 """
1995 if not self.GetIssue():
1996 return None
1997
1998 try:
1999 props = self.GetIssueProperties()
2000 except urllib2.HTTPError:
2001 return 'error'
2002
2003 if props.get('closed'):
2004 # Issue is closed.
2005 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002006 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002007 # Issue is in the commit queue.
2008 return 'commit'
2009
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002010 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002011 if not messages:
2012 # No message was sent.
2013 return 'unsent'
2014
2015 if get_approving_reviewers(props):
2016 return 'lgtm'
2017 elif get_approving_reviewers(props, disapproval=True):
2018 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002019
tandrii9d2c7a32016-06-22 03:42:45 -07002020 # Skip CQ messages that don't require owner's action.
2021 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2022 if 'Dry run:' in messages[-1]['text']:
2023 messages.pop()
2024 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2025 # This message always follows prior messages from CQ,
2026 # so skip this too.
2027 messages.pop()
2028 else:
2029 # This is probably a CQ messages warranting user attention.
2030 break
2031
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002032 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002033 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002034 return 'reply'
2035 return 'waiting'
2036
dsansomee2d6fd92016-09-08 00:10:47 -07002037 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002038 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002039
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002040 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002041 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002042
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002043 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002044 return self.SetFlags({flag: value})
2045
2046 def SetFlags(self, flags):
2047 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002048 """
phajdan.jr68598232016-08-10 03:28:28 -07002049 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002050 try:
tandrii4b233bd2016-07-06 03:50:29 -07002051 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002052 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002053 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002054 if e.code == 404:
2055 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2056 if e.code == 403:
2057 DieWithError(
2058 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002059 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002060 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002061
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002062 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002063 """Returns an upload.RpcServer() to access this review's rietveld instance.
2064 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002065 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002066 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002067 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002068 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002069 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002071 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002072 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002073 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002074
tandrii5d48c322016-08-18 16:19:37 -07002075 @classmethod
2076 def PatchsetConfigKey(cls):
2077 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002078
tandrii5d48c322016-08-18 16:19:37 -07002079 @classmethod
2080 def CodereviewServerConfigKey(cls):
2081 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002082
Ravi Mistry31e7d562018-04-02 12:53:57 -04002083 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2084 raise NotImplementedError()
2085
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002086 def SetCQState(self, new_state):
2087 props = self.GetIssueProperties()
2088 if props.get('private'):
2089 DieWithError('Cannot set-commit on private issue')
2090
2091 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002092 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002093 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002094 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002095 else:
tandrii4b233bd2016-07-06 03:50:29 -07002096 assert new_state == _CQState.DRY_RUN
2097 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002098
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002099 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002100 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002101 # PatchIssue should never be called with a dirty tree. It is up to the
2102 # caller to check this, but just in case we assert here since the
2103 # consequences of the caller not checking this could be dire.
2104 assert(not git_common.is_dirty_git_tree('apply'))
2105 assert(parsed_issue_arg.valid)
2106 self._changelist.issue = parsed_issue_arg.issue
2107 if parsed_issue_arg.hostname:
2108 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2109
skobes6468b902016-10-24 08:45:10 -07002110 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2111 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2112 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002113 try:
skobes6468b902016-10-24 08:45:10 -07002114 scm_obj.apply_patch(patchset_object)
2115 except Exception as e:
2116 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002117 return 1
2118
2119 # If we had an issue, commit the current state and register the issue.
2120 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002121 self.SetIssue(self.GetIssue())
2122 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002123 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2124 'patch from issue %(i)s at patchset '
2125 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2126 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002127 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002128 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002129 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002130 return 0
2131
2132 @staticmethod
2133 def ParseIssueURL(parsed_url):
2134 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2135 return None
wychen3c1c1722016-08-04 11:46:36 -07002136 # Rietveld patch: https://domain/<number>/#ps<patchset>
2137 match = re.match(r'/(\d+)/$', parsed_url.path)
2138 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2139 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002140 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002141 issue=int(match.group(1)),
2142 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002143 hostname=parsed_url.netloc,
2144 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002145 # Typical url: https://domain/<issue_number>[/[other]]
2146 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2147 if match:
skobes6468b902016-10-24 08:45:10 -07002148 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002149 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002150 hostname=parsed_url.netloc,
2151 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002152 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2153 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2154 if match:
skobes6468b902016-10-24 08:45:10 -07002155 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002156 issue=int(match.group(1)),
2157 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002158 hostname=parsed_url.netloc,
2159 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002160 return None
2161
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002162 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002163 """Upload the patch to Rietveld."""
2164 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2165 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002166 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2167 if options.emulate_svn_auto_props:
2168 upload_args.append('--emulate_svn_auto_props')
2169
2170 change_desc = None
2171
2172 if options.email is not None:
2173 upload_args.extend(['--email', options.email])
2174
2175 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002176 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002177 upload_args.extend(['--title', options.title])
2178 if options.message:
2179 upload_args.extend(['--message', options.message])
2180 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002181 print('This branch is associated with issue %s. '
2182 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002183 else:
nodirca166002016-06-27 10:59:51 -07002184 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002185 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002186 if options.message:
2187 message = options.message
2188 else:
2189 message = CreateDescriptionFromLog(args)
2190 if options.title:
2191 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002192 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002193 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002194 change_desc.update_reviewers(options.reviewers, options.tbrs,
2195 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002196 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002197 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002198
2199 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002200 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002201 return 1
2202
2203 upload_args.extend(['--message', change_desc.description])
2204 if change_desc.get_reviewers():
2205 upload_args.append('--reviewers=%s' % ','.join(
2206 change_desc.get_reviewers()))
2207 if options.send_mail:
2208 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002209 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002210 upload_args.append('--send_mail')
2211
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00002212 # We only skip auto-CC-ing addresses from rietveld.cc when --private or
2213 # --no-autocc is explicitly specified on the command line. Should private
2214 # CL be created due to rietveld.private value, we assume that rietveld.cc
2215 # only contains addresses where private CLs are allowed to be sent.
2216 if options.private or options.no_autocc:
2217 logging.warn('rietveld.cc is ignored since private/no-autocc flag is '
2218 'specified. You need to review and add them manually if '
2219 'necessary.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 cc = self.GetCCListWithoutDefault()
2221 else:
2222 cc = self.GetCCList()
2223 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002224 if change_desc.get_cced():
2225 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002226 if cc:
2227 upload_args.extend(['--cc', cc])
2228
2229 if options.private or settings.GetDefaultPrivateFlag() == "True":
2230 upload_args.append('--private')
2231
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002232 # Include the upstream repo's URL in the change -- this is useful for
2233 # projects that have their source spread across multiple repos.
2234 remote_url = self.GetGitBaseUrlFromConfig()
2235 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002236 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2237 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2238 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002239 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002240 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002241 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002242 if target_ref:
2243 upload_args.extend(['--target_ref', target_ref])
2244
2245 # Look for dependent patchsets. See crbug.com/480453 for more details.
2246 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2247 upstream_branch = ShortBranchName(upstream_branch)
2248 if remote is '.':
2249 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002250 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002251 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002252 print()
2253 print('Skipping dependency patchset upload because git config '
2254 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2255 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002256 else:
2257 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002258 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002259 auth_config=auth_config)
2260 branch_cl_issue_url = branch_cl.GetIssueURL()
2261 branch_cl_issue = branch_cl.GetIssue()
2262 branch_cl_patchset = branch_cl.GetPatchset()
2263 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2264 upload_args.extend(
2265 ['--depends_on_patchset', '%s:%s' % (
2266 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002267 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002268 '\n'
2269 'The current branch (%s) is tracking a local branch (%s) with '
2270 'an associated CL.\n'
2271 'Adding %s/#ps%s as a dependency patchset.\n'
2272 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2273 branch_cl_patchset))
2274
2275 project = settings.GetProject()
2276 if project:
2277 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002278 else:
2279 print()
2280 print('WARNING: Uploading without a project specified. Please ensure '
2281 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2282 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002283
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002284 try:
2285 upload_args = ['upload'] + upload_args + args
2286 logging.info('upload.RealMain(%s)', upload_args)
2287 issue, patchset = upload.RealMain(upload_args)
2288 issue = int(issue)
2289 patchset = int(patchset)
2290 except KeyboardInterrupt:
2291 sys.exit(1)
2292 except:
2293 # If we got an exception after the user typed a description for their
2294 # change, back up the description before re-raising.
2295 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002296 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002297 raise
2298
2299 if not self.GetIssue():
2300 self.SetIssue(issue)
2301 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002302 return 0
2303
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002304
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002305class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002306 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002307 # auth_config is Rietveld thing, kept here to preserve interface only.
2308 super(_GerritChangelistImpl, self).__init__(changelist)
2309 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002310 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002311 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002312 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002313 # Map from change number (issue) to its detail cache.
2314 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002315
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002316 if codereview_host is not None:
2317 assert not codereview_host.startswith('https://'), codereview_host
2318 self._gerrit_host = codereview_host
2319 self._gerrit_server = 'https://%s' % codereview_host
2320
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002321 def _GetGerritHost(self):
2322 # Lazy load of configs.
2323 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002324 if self._gerrit_host and '.' not in self._gerrit_host:
2325 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2326 # This happens for internal stuff http://crbug.com/614312.
2327 parsed = urlparse.urlparse(self.GetRemoteUrl())
2328 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002329 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002330 ' Your current remote is: %s' % self.GetRemoteUrl())
2331 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2332 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002333 return self._gerrit_host
2334
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002335 def _GetGitHost(self):
2336 """Returns git host to be used when uploading change to Gerrit."""
2337 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2338
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002339 def GetCodereviewServer(self):
2340 if not self._gerrit_server:
2341 # If we're on a branch then get the server potentially associated
2342 # with that branch.
2343 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002344 self._gerrit_server = self._GitGetBranchConfigValue(
2345 self.CodereviewServerConfigKey())
2346 if self._gerrit_server:
2347 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002348 if not self._gerrit_server:
2349 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2350 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002351 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002352 parts[0] = parts[0] + '-review'
2353 self._gerrit_host = '.'.join(parts)
2354 self._gerrit_server = 'https://%s' % self._gerrit_host
2355 return self._gerrit_server
2356
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002357 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002358 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002359 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002360 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002361 logging.warn('can\'t detect Gerrit project.')
2362 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002363 project = urlparse.urlparse(remote_url).path.strip('/')
2364 if project.endswith('.git'):
2365 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00002366 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
2367 # 'a/' prefix, because 'a/' prefix is used to force authentication in
2368 # gitiles/git-over-https protocol. E.g.,
2369 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
2370 # as
2371 # https://chromium.googlesource.com/v8/v8
2372 if project.startswith('a/'):
2373 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002374 return project
2375
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002376 def _GerritChangeIdentifier(self):
2377 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2378
2379 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002380 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002381 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002382 project = self._GetGerritProject()
2383 if project:
2384 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2385 # Fall back on still unique, but less efficient change number.
2386 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002387
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002388 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002389 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002390 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002391
tandrii5d48c322016-08-18 16:19:37 -07002392 @classmethod
2393 def PatchsetConfigKey(cls):
2394 return 'gerritpatchset'
2395
2396 @classmethod
2397 def CodereviewServerConfigKey(cls):
2398 return 'gerritserver'
2399
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002400 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002401 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002402 if settings.GetGerritSkipEnsureAuthenticated():
2403 # For projects with unusual authentication schemes.
2404 # See http://crbug.com/603378.
2405 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002406 # Lazy-loader to identify Gerrit and Git hosts.
2407 if gerrit_util.GceAuthenticator.is_gce():
2408 return
2409 self.GetCodereviewServer()
2410 git_host = self._GetGitHost()
2411 assert self._gerrit_server and self._gerrit_host
2412 cookie_auth = gerrit_util.CookiesAuthenticator()
2413
2414 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2415 git_auth = cookie_auth.get_auth_header(git_host)
2416 if gerrit_auth and git_auth:
2417 if gerrit_auth == git_auth:
2418 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002419 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002420 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002421 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002422 ' %s\n'
2423 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002424 ' Consider running the following command:\n'
2425 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002426 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002427 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002428 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002429 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002430 cookie_auth.get_new_password_message(git_host)))
2431 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002432 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002433 return
2434 else:
2435 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002436 ([] if gerrit_auth else [self._gerrit_host]) +
2437 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002438 DieWithError('Credentials for the following hosts are required:\n'
2439 ' %s\n'
2440 'These are read from %s (or legacy %s)\n'
2441 '%s' % (
2442 '\n '.join(missing),
2443 cookie_auth.get_gitcookies_path(),
2444 cookie_auth.get_netrc_path(),
2445 cookie_auth.get_new_password_message(git_host)))
2446
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002447 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002448 if not self.GetIssue():
2449 return
2450
2451 # Warm change details cache now to avoid RPCs later, reducing latency for
2452 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002453 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002454 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002455
2456 status = self._GetChangeDetail()['status']
2457 if status in ('MERGED', 'ABANDONED'):
2458 DieWithError('Change %s has been %s, new uploads are not allowed' %
2459 (self.GetIssueURL(),
2460 'submitted' if status == 'MERGED' else 'abandoned'))
2461
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002462 if gerrit_util.GceAuthenticator.is_gce():
2463 return
2464 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2465 self._GetGerritHost())
2466 if self.GetIssueOwner() == cookies_user:
2467 return
2468 logging.debug('change %s owner is %s, cookies user is %s',
2469 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002470 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002471 # so ask what Gerrit thinks of this user.
2472 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2473 if details['email'] == self.GetIssueOwner():
2474 return
2475 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002476 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002477 'as %s.\n'
2478 'Uploading may fail due to lack of permissions.' %
2479 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2480 confirm_or_exit(action='upload')
2481
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002482 def _PostUnsetIssueProperties(self):
2483 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002484 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002485
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002486 def GetGerritObjForPresubmit(self):
2487 return presubmit_support.GerritAccessor(self._GetGerritHost())
2488
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002489 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002490 """Apply a rough heuristic to give a simple summary of an issue's review
2491 or CQ status, assuming adherence to a common workflow.
2492
2493 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002494 * 'error' - error from review tool (including deleted issues)
2495 * 'unsent' - no reviewers added
2496 * 'waiting' - waiting for review
2497 * 'reply' - waiting for uploader to reply to review
2498 * 'lgtm' - Code-Review label has been set
2499 * 'commit' - in the commit queue
2500 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002501 """
2502 if not self.GetIssue():
2503 return None
2504
2505 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002506 data = self._GetChangeDetail([
2507 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002508 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002509 return 'error'
2510
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002511 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002512 return 'closed'
2513
Aaron Gable9ab38c62017-04-06 14:36:33 -07002514 if data['labels'].get('Commit-Queue', {}).get('approved'):
2515 # The section will have an "approved" subsection if anyone has voted
2516 # the maximum value on the label.
2517 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002518
Aaron Gable9ab38c62017-04-06 14:36:33 -07002519 if data['labels'].get('Code-Review', {}).get('approved'):
2520 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002521
2522 if not data.get('reviewers', {}).get('REVIEWER', []):
2523 return 'unsent'
2524
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002525 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002526 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2527 last_message_author = messages.pop().get('author', {})
2528 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002529 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2530 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002531 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002532 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002533 if last_message_author.get('_account_id') == owner:
2534 # Most recent message was by owner.
2535 return 'waiting'
2536 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002537 # Some reply from non-owner.
2538 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002539
2540 # Somehow there are no messages even though there are reviewers.
2541 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002542
2543 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002544 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002545 patchset = data['revisions'][data['current_revision']]['_number']
2546 self.SetPatchset(patchset)
2547 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002548
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002549 def FetchDescription(self, force=False):
2550 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2551 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002552 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002553 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002554
dsansomee2d6fd92016-09-08 00:10:47 -07002555 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002556 if gerrit_util.HasPendingChangeEdit(
2557 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002558 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002559 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002560 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002561 'unpublished edit. Either publish the edit in the Gerrit web UI '
2562 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002563
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002564 gerrit_util.DeletePendingChangeEdit(
2565 self._GetGerritHost(), self._GerritChangeIdentifier())
2566 gerrit_util.SetCommitMessage(
2567 self._GetGerritHost(), self._GerritChangeIdentifier(),
2568 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002569
Aaron Gable636b13f2017-07-14 10:42:48 -07002570 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002571 gerrit_util.SetReview(
2572 self._GetGerritHost(), self._GerritChangeIdentifier(),
2573 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002574
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002575 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002576 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002577 messages = self._GetChangeDetail(
2578 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2579 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002580 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002581
2582 # Build dictionary of file comments for easy access and sorting later.
2583 # {author+date: {path: {patchset: {line: url+message}}}}
2584 comments = collections.defaultdict(
2585 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2586 for path, line_comments in file_comments.iteritems():
2587 for comment in line_comments:
2588 if comment.get('tag', '').startswith('autogenerated'):
2589 continue
2590 key = (comment['author']['email'], comment['updated'])
2591 if comment.get('side', 'REVISION') == 'PARENT':
2592 patchset = 'Base'
2593 else:
2594 patchset = 'PS%d' % comment['patch_set']
2595 line = comment.get('line', 0)
2596 url = ('https://%s/c/%s/%s/%s#%s%s' %
2597 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2598 'b' if comment.get('side') == 'PARENT' else '',
2599 str(line) if line else ''))
2600 comments[key][path][patchset][line] = (url, comment['message'])
2601
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002602 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002603 for msg in messages:
2604 # Don't bother showing autogenerated messages.
2605 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2606 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002607 # Gerrit spits out nanoseconds.
2608 assert len(msg['date'].split('.')[-1]) == 9
2609 date = datetime.datetime.strptime(msg['date'][:-3],
2610 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002611 message = msg['message']
2612 key = (msg['author']['email'], msg['date'])
2613 if key in comments:
2614 message += '\n'
2615 for path, patchsets in sorted(comments.get(key, {}).items()):
2616 if readable:
2617 message += '\n%s' % path
2618 for patchset, lines in sorted(patchsets.items()):
2619 for line, (url, content) in sorted(lines.items()):
2620 if line:
2621 line_str = 'Line %d' % line
2622 path_str = '%s:%d:' % (path, line)
2623 else:
2624 line_str = 'File comment'
2625 path_str = '%s:0:' % path
2626 if readable:
2627 message += '\n %s, %s: %s' % (patchset, line_str, url)
2628 message += '\n %s\n' % content
2629 else:
2630 message += '\n%s ' % path_str
2631 message += '\n%s\n' % content
2632
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002633 summary.append(_CommentSummary(
2634 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002635 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002636 sender=msg['author']['email'],
2637 # These could be inferred from the text messages and correlated with
2638 # Code-Review label maximum, however this is not reliable.
2639 # Leaving as is until the need arises.
2640 approval=False,
2641 disapproval=False,
2642 ))
2643 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002644
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002645 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002646 gerrit_util.AbandonChange(
2647 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002648
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002649 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002650 gerrit_util.SubmitChange(
2651 self._GetGerritHost(), self._GerritChangeIdentifier(),
2652 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002653
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002654 def _GetChangeDetail(self, options=None, no_cache=False):
2655 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002656
2657 If fresh data is needed, set no_cache=True which will clear cache and
2658 thus new data will be fetched from Gerrit.
2659 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002660 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002661 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002662
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002663 # Optimization to avoid multiple RPCs:
2664 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2665 'CURRENT_COMMIT' not in options):
2666 options.append('CURRENT_COMMIT')
2667
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002668 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002669 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002670 options = [o.upper() for o in options]
2671
2672 # Check in cache first unless no_cache is True.
2673 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002674 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002675 else:
2676 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002677 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002678 # Assumption: data fetched before with extra options is suitable
2679 # for return for a smaller set of options.
2680 # For example, if we cached data for
2681 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2682 # and request is for options=[CURRENT_REVISION],
2683 # THEN we can return prior cached data.
2684 if options_set.issubset(cached_options_set):
2685 return data
2686
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002687 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002688 data = gerrit_util.GetChangeDetail(
2689 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002690 except gerrit_util.GerritError as e:
2691 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002692 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002693 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002694
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002695 self._detail_cache.setdefault(cache_key, []).append(
2696 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002697 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002698
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002699 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002700 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002701 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002702 data = gerrit_util.GetChangeCommit(
2703 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002704 except gerrit_util.GerritError as e:
2705 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002706 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002707 raise
agable32978d92016-11-01 12:55:02 -07002708 return data
2709
Olivier Robin75ee7252018-04-13 10:02:56 +02002710 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002711 if git_common.is_dirty_git_tree('land'):
2712 return 1
tandriid60367b2016-06-22 05:25:12 -07002713 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2714 if u'Commit-Queue' in detail.get('labels', {}):
2715 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002716 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2717 'which can test and land changes for you. '
2718 'Are you sure you wish to bypass it?\n',
2719 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002720
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002721 differs = True
tandriic4344b52016-08-29 06:04:54 -07002722 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002723 # Note: git diff outputs nothing if there is no diff.
2724 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002725 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002726 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002727 if detail['current_revision'] == last_upload:
2728 differs = False
2729 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002730 print('WARNING: Local branch contents differ from latest uploaded '
2731 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002732 if differs:
2733 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002734 confirm_or_exit(
2735 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2736 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002737 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002738 elif not bypass_hooks:
2739 hook_results = self.RunHook(
2740 committing=True,
2741 may_prompt=not force,
2742 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002743 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2744 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002745 if not hook_results.should_continue():
2746 return 1
2747
2748 self.SubmitIssue(wait_for_merge=True)
2749 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002750 links = self._GetChangeCommit().get('web_links', [])
2751 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002752 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002753 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002754 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002755 return 0
2756
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002757 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002758 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002759 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002760 assert not directory
2761 assert parsed_issue_arg.valid
2762
2763 self._changelist.issue = parsed_issue_arg.issue
2764
2765 if parsed_issue_arg.hostname:
2766 self._gerrit_host = parsed_issue_arg.hostname
2767 self._gerrit_server = 'https://%s' % self._gerrit_host
2768
tandriic2405f52016-10-10 08:13:15 -07002769 try:
2770 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002771 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002772 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002773
2774 if not parsed_issue_arg.patchset:
2775 # Use current revision by default.
2776 revision_info = detail['revisions'][detail['current_revision']]
2777 patchset = int(revision_info['_number'])
2778 else:
2779 patchset = parsed_issue_arg.patchset
2780 for revision_info in detail['revisions'].itervalues():
2781 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2782 break
2783 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002784 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002785 (parsed_issue_arg.patchset, self.GetIssue()))
2786
Aaron Gable697a91b2018-01-19 15:20:15 -08002787 remote_url = self._changelist.GetRemoteUrl()
2788 if remote_url.endswith('.git'):
2789 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002790 remote_url = remote_url.rstrip('/')
2791
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002792 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002793 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002794
2795 if remote_url != fetch_info['url']:
2796 DieWithError('Trying to patch a change from %s but this repo appears '
2797 'to be %s.' % (fetch_info['url'], remote_url))
2798
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002799 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002800
Aaron Gable62619a32017-06-16 08:22:09 -07002801 if force:
2802 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2803 print('Checked out commit for change %i patchset %i locally' %
2804 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002805 elif nocommit:
2806 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2807 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002808 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002809 RunGit(['cherry-pick', 'FETCH_HEAD'])
2810 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002811 (parsed_issue_arg.issue, patchset))
2812 print('Note: this created a local commit which does not have '
2813 'the same hash as the one uploaded for review. This will make '
2814 'uploading changes based on top of this branch difficult.\n'
2815 'If you want to do that, use "git cl patch --force" instead.')
2816
Stefan Zagerd08043c2017-10-12 12:07:02 -07002817 if self.GetBranch():
2818 self.SetIssue(parsed_issue_arg.issue)
2819 self.SetPatchset(patchset)
2820 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2821 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2822 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2823 else:
2824 print('WARNING: You are in detached HEAD state.\n'
2825 'The patch has been applied to your checkout, but you will not be '
2826 'able to upload a new patch set to the gerrit issue.\n'
2827 'Try using the \'-b\' option if you would like to work on a '
2828 'branch and/or upload a new patch set.')
2829
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002830 return 0
2831
2832 @staticmethod
2833 def ParseIssueURL(parsed_url):
2834 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2835 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002836 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2837 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002838 # Short urls like https://domain/<issue_number> can be used, but don't allow
2839 # specifying the patchset (you'd 404), but we allow that here.
2840 if parsed_url.path == '/':
2841 part = parsed_url.fragment
2842 else:
2843 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002844 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002845 if match:
2846 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002847 issue=int(match.group(3)),
2848 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002849 hostname=parsed_url.netloc,
2850 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002851 return None
2852
tandrii16e0b4e2016-06-07 10:34:28 -07002853 def _GerritCommitMsgHookCheck(self, offer_removal):
2854 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2855 if not os.path.exists(hook):
2856 return
2857 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2858 # custom developer made one.
2859 data = gclient_utils.FileRead(hook)
2860 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2861 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002862 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002863 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002864 'and may interfere with it in subtle ways.\n'
2865 'We recommend you remove the commit-msg hook.')
2866 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002867 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002868 gclient_utils.rm_file_or_tree(hook)
2869 print('Gerrit commit-msg hook removed.')
2870 else:
2871 print('OK, will keep Gerrit commit-msg hook in place.')
2872
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002873 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002874 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002875 if options.squash and options.no_squash:
2876 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002877
2878 if not options.squash and not options.no_squash:
2879 # Load default for user, repo, squash=true, in this order.
2880 options.squash = settings.GetSquashGerritUploads()
2881 elif options.no_squash:
2882 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002883
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002884 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002885 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002886
Aaron Gableb56ad332017-01-06 15:24:31 -08002887 # This may be None; default fallback value is determined in logic below.
2888 title = options.title
2889
Dominic Battre7d1c4842017-10-27 09:17:28 +02002890 # Extract bug number from branch name.
2891 bug = options.bug
2892 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2893 if not bug and match:
2894 bug = match.group(1)
2895
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002896 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002897 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002898 if self.GetIssue():
2899 # Try to get the message from a previous upload.
2900 message = self.GetDescription()
2901 if not message:
2902 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002903 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002904 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002905 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002906 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002907 # When uploading a subsequent patchset, -m|--message is taken
2908 # as the patchset title if --title was not provided.
2909 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002910 else:
2911 default_title = RunGit(
2912 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002913 if options.force:
2914 title = default_title
2915 else:
2916 title = ask_for_data(
2917 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002918 change_id = self._GetChangeDetail()['change_id']
2919 while True:
2920 footer_change_ids = git_footers.get_footer_change_id(message)
2921 if footer_change_ids == [change_id]:
2922 break
2923 if not footer_change_ids:
2924 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002925 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002926 continue
2927 # There is already a valid footer but with different or several ids.
2928 # Doing this automatically is non-trivial as we don't want to lose
2929 # existing other footers, yet we want to append just 1 desired
2930 # Change-Id. Thus, just create a new footer, but let user verify the
2931 # new description.
2932 message = '%s\n\nChange-Id: %s' % (message, change_id)
2933 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002934 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002935 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002936 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002937 'Please, check the proposed correction to the description, '
2938 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2939 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2940 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002941 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002942 if not options.force:
2943 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002944 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002945 message = change_desc.description
2946 if not message:
2947 DieWithError("Description is empty. Aborting...")
2948 # Continue the while loop.
2949 # Sanity check of this code - we should end up with proper message
2950 # footer.
2951 assert [change_id] == git_footers.get_footer_change_id(message)
2952 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002953 else: # if not self.GetIssue()
2954 if options.message:
2955 message = options.message
2956 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002957 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002958 if options.title:
2959 message = options.title + '\n\n' + message
2960 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002961
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002962 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002963 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002964 # On first upload, patchset title is always this string, while
2965 # --title flag gets converted to first line of message.
2966 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002967 if not change_desc.description:
2968 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002969 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002970 if len(change_ids) > 1:
2971 DieWithError('too many Change-Id footers, at most 1 allowed.')
2972 if not change_ids:
2973 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002974 change_desc.set_description(git_footers.add_footer_change_id(
2975 change_desc.description,
2976 GenerateGerritChangeId(change_desc.description)))
2977 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002978 assert len(change_ids) == 1
2979 change_id = change_ids[0]
2980
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002981 if options.reviewers or options.tbrs or options.add_owners_to:
2982 change_desc.update_reviewers(options.reviewers, options.tbrs,
2983 options.add_owners_to, change)
2984
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002985 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002986 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2987 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002988 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002989 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2990 desc_tempfile.write(change_desc.description)
2991 desc_tempfile.close()
2992 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2993 '-F', desc_tempfile.name]).strip()
2994 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002995 else:
2996 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002997 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002998 if not change_desc.description:
2999 DieWithError("Description is empty. Aborting...")
3000
3001 if not git_footers.get_footer_change_id(change_desc.description):
3002 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003003 change_desc.set_description(
3004 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003005 if options.reviewers or options.tbrs or options.add_owners_to:
3006 change_desc.update_reviewers(options.reviewers, options.tbrs,
3007 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003008 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003009 # For no-squash mode, we assume the remote called "origin" is the one we
3010 # want. It is not worthwhile to support different workflows for
3011 # no-squash mode.
3012 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003013 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3014
3015 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00003016 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003017 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3018 ref_to_push)]).splitlines()
3019 if len(commits) > 1:
3020 print('WARNING: This will upload %d commits. Run the following command '
3021 'to see which commits will be uploaded: ' % len(commits))
3022 print('git log %s..%s' % (parent, ref_to_push))
3023 print('You can also use `git squash-branch` to squash these into a '
3024 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003025 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003026
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003027 if options.reviewers or options.tbrs or options.add_owners_to:
3028 change_desc.update_reviewers(options.reviewers, options.tbrs,
3029 options.add_owners_to, change)
3030
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003031 # Extra options that can be specified at push time. Doc:
3032 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003033 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003034
Aaron Gable844cf292017-06-28 11:32:59 -07003035 # By default, new changes are started in WIP mode, and subsequent patchsets
3036 # don't send email. At any time, passing --send-mail will mark the change
3037 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003038 if options.send_mail:
3039 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003040 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04003041 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003042 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003043 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003044 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003045
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003046 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003047 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003048
Aaron Gable9b713dd2016-12-14 16:04:21 -08003049 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003050 # Punctuation and whitespace in |title| must be percent-encoded.
3051 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003052
agablec6787972016-09-09 16:13:34 -07003053 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003054 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003055
rmistry9eadede2016-09-19 11:22:43 -07003056 if options.topic:
3057 # Documentation on Gerrit topics is here:
3058 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003059 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003060
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003061 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003062 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003063 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003064 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003065 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3066
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003067 refspec_suffix = ''
3068 if refspec_opts:
3069 refspec_suffix = '%' + ','.join(refspec_opts)
3070 assert ' ' not in refspec_suffix, (
3071 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3072 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3073
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003074 try:
3075 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00003076 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003077 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003078 # Flush after every line: useful for seeing progress when running as
3079 # recipe.
3080 filter_fn=lambda _: sys.stdout.flush())
3081 except subprocess2.CalledProcessError:
3082 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003083 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003084 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003085 'credential problems:\n'
3086 ' git cl creds-check\n',
3087 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003088
3089 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003090 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003091 change_numbers = [m.group(1)
3092 for m in map(regex.match, push_stdout.splitlines())
3093 if m]
3094 if len(change_numbers) != 1:
3095 DieWithError(
3096 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003097 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003098 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003099 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003100
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003101 reviewers = sorted(change_desc.get_reviewers())
3102
tandrii88189772016-09-29 04:29:57 -07003103 # Add cc's from the CC_LIST and --cc flag (if any).
Sergiy Byelozyorovaaf2cc02018-09-24 18:02:28 +00003104 if not options.private and not options.no_autocc:
Aaron Gabled1052492017-05-15 15:05:34 -07003105 cc = self.GetCCList().split(',')
3106 else:
3107 cc = []
tandrii88189772016-09-29 04:29:57 -07003108 if options.cc:
3109 cc.extend(options.cc)
3110 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003111 if change_desc.get_cced():
3112 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003113
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003114 if self.GetIssue():
3115 # GetIssue() is not set in case of non-squash uploads according to tests.
3116 # TODO(agable): non-squash uploads in git cl should be removed.
3117 gerrit_util.AddReviewers(
3118 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003119 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003120 reviewers, cc,
3121 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003122
Aaron Gablefd238082017-06-07 13:42:34 -07003123 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003124 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3125 score = 1
3126 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3127 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3128 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003129 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003130 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003131 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003132 msg='Self-approving for TBR',
3133 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003134
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003135 return 0
3136
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003137 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3138 change_desc):
3139 """Computes parent of the generated commit to be uploaded to Gerrit.
3140
3141 Returns revision or a ref name.
3142 """
3143 if custom_cl_base:
3144 # Try to avoid creating additional unintended CLs when uploading, unless
3145 # user wants to take this risk.
3146 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3147 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3148 local_ref_of_target_remote])
3149 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003150 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003151 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3152 'If you proceed with upload, more than 1 CL may be created by '
3153 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3154 'If you are certain that specified base `%s` has already been '
3155 'uploaded to Gerrit as another CL, you may proceed.\n' %
3156 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3157 if not force:
3158 confirm_or_exit(
3159 'Do you take responsibility for cleaning up potential mess '
3160 'resulting from proceeding with upload?',
3161 action='upload')
3162 return custom_cl_base
3163
Aaron Gablef97e33d2017-03-30 15:44:27 -07003164 if remote != '.':
3165 return self.GetCommonAncestorWithUpstream()
3166
3167 # If our upstream branch is local, we base our squashed commit on its
3168 # squashed version.
3169 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3170
Aaron Gablef97e33d2017-03-30 15:44:27 -07003171 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003172 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003173
3174 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003175 # TODO(tandrii): consider checking parent change in Gerrit and using its
3176 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3177 # the tree hash of the parent branch. The upside is less likely bogus
3178 # requests to reupload parent change just because it's uploadhash is
3179 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003180 parent = RunGit(['config',
3181 'branch.%s.gerritsquashhash' % upstream_branch_name],
3182 error_ok=True).strip()
3183 # Verify that the upstream branch has been uploaded too, otherwise
3184 # Gerrit will create additional CLs when uploading.
3185 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3186 RunGitSilent(['rev-parse', parent + ':'])):
3187 DieWithError(
3188 '\nUpload upstream branch %s first.\n'
3189 'It is likely that this branch has been rebased since its last '
3190 'upload, so you just need to upload it again.\n'
3191 '(If you uploaded it with --no-squash, then branch dependencies '
3192 'are not supported, and you should reupload with --squash.)'
3193 % upstream_branch_name,
3194 change_desc)
3195 return parent
3196
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003197 def _AddChangeIdToCommitMessage(self, options, args):
3198 """Re-commits using the current message, assumes the commit hook is in
3199 place.
3200 """
3201 log_desc = options.message or CreateDescriptionFromLog(args)
3202 git_command = ['commit', '--amend', '-m', log_desc]
3203 RunGit(git_command)
3204 new_log_desc = CreateDescriptionFromLog(args)
3205 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003206 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003207 return new_log_desc
3208 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003209 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003210
Ravi Mistry31e7d562018-04-02 12:53:57 -04003211 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3212 """Sets labels on the change based on the provided flags."""
3213 labels = {}
3214 notify = None;
3215 if enable_auto_submit:
3216 labels['Auto-Submit'] = 1
3217 if use_commit_queue:
3218 labels['Commit-Queue'] = 2
3219 elif cq_dry_run:
3220 labels['Commit-Queue'] = 1
3221 notify = False
3222 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003223 gerrit_util.SetReview(
3224 self._GetGerritHost(),
3225 self._GerritChangeIdentifier(),
3226 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003227
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003228 def SetCQState(self, new_state):
3229 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003230 vote_map = {
3231 _CQState.NONE: 0,
3232 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003233 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003234 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003235 labels = {'Commit-Queue': vote_map[new_state]}
3236 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003237 gerrit_util.SetReview(
3238 self._GetGerritHost(), self._GerritChangeIdentifier(),
3239 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003240
tandriie113dfd2016-10-11 10:20:12 -07003241 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003242 try:
3243 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003244 except GerritChangeNotExists:
3245 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003246
3247 if data['status'] in ('ABANDONED', 'MERGED'):
3248 return 'CL %s is closed' % self.GetIssue()
3249
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003250 def GetTryJobProperties(self, patchset=None):
3251 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003252 data = self._GetChangeDetail(['ALL_REVISIONS'])
3253 patchset = int(patchset or self.GetPatchset())
3254 assert patchset
3255 revision_data = None # Pylint wants it to be defined.
3256 for revision_data in data['revisions'].itervalues():
3257 if int(revision_data['_number']) == patchset:
3258 break
3259 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003260 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003261 (patchset, self.GetIssue()))
3262 return {
3263 'patch_issue': self.GetIssue(),
3264 'patch_set': patchset or self.GetPatchset(),
3265 'patch_project': data['project'],
3266 'patch_storage': 'gerrit',
3267 'patch_ref': revision_data['fetch']['http']['ref'],
3268 'patch_repository_url': revision_data['fetch']['http']['url'],
3269 'patch_gerrit_url': self.GetCodereviewServer(),
3270 }
tandriie113dfd2016-10-11 10:20:12 -07003271
tandriide281ae2016-10-12 06:02:30 -07003272 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003273 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003274
Edward Lemur707d70b2018-02-07 00:50:14 +01003275 def GetReviewers(self):
3276 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3277 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3278
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003279
3280_CODEREVIEW_IMPLEMENTATIONS = {
3281 'rietveld': _RietveldChangelistImpl,
3282 'gerrit': _GerritChangelistImpl,
3283}
3284
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003285
iannuccie53c9352016-08-17 14:40:40 -07003286def _add_codereview_issue_select_options(parser, extra=""):
3287 _add_codereview_select_options(parser)
3288
3289 text = ('Operate on this issue number instead of the current branch\'s '
3290 'implicit issue.')
3291 if extra:
3292 text += ' '+extra
3293 parser.add_option('-i', '--issue', type=int, help=text)
3294
3295
3296def _process_codereview_issue_select_options(parser, options):
3297 _process_codereview_select_options(parser, options)
3298 if options.issue is not None and not options.forced_codereview:
3299 parser.error('--issue must be specified with either --rietveld or --gerrit')
3300
3301
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003302def _add_codereview_select_options(parser):
3303 """Appends --gerrit and --rietveld options to force specific codereview."""
3304 parser.codereview_group = optparse.OptionGroup(
3305 parser, 'EXPERIMENTAL! Codereview override options')
3306 parser.add_option_group(parser.codereview_group)
3307 parser.codereview_group.add_option(
3308 '--gerrit', action='store_true',
3309 help='Force the use of Gerrit for codereview')
3310 parser.codereview_group.add_option(
3311 '--rietveld', action='store_true',
3312 help='Force the use of Rietveld for codereview')
3313
3314
3315def _process_codereview_select_options(parser, options):
3316 if options.gerrit and options.rietveld:
3317 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3318 options.forced_codereview = None
3319 if options.gerrit:
3320 options.forced_codereview = 'gerrit'
3321 elif options.rietveld:
3322 options.forced_codereview = 'rietveld'
3323
3324
tandriif9aefb72016-07-01 09:06:51 -07003325def _get_bug_line_values(default_project, bugs):
3326 """Given default_project and comma separated list of bugs, yields bug line
3327 values.
3328
3329 Each bug can be either:
3330 * a number, which is combined with default_project
3331 * string, which is left as is.
3332
3333 This function may produce more than one line, because bugdroid expects one
3334 project per line.
3335
3336 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3337 ['v8:123', 'chromium:789']
3338 """
3339 default_bugs = []
3340 others = []
3341 for bug in bugs.split(','):
3342 bug = bug.strip()
3343 if bug:
3344 try:
3345 default_bugs.append(int(bug))
3346 except ValueError:
3347 others.append(bug)
3348
3349 if default_bugs:
3350 default_bugs = ','.join(map(str, default_bugs))
3351 if default_project:
3352 yield '%s:%s' % (default_project, default_bugs)
3353 else:
3354 yield default_bugs
3355 for other in sorted(others):
3356 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3357 yield other
3358
3359
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003360class ChangeDescription(object):
3361 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003362 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003363 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003364 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003365 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003366 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3367 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3368 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3369 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003370
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003371 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003372 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003373
agable@chromium.org42c20792013-09-12 17:34:49 +00003374 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003375 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003376 return '\n'.join(self._description_lines)
3377
3378 def set_description(self, desc):
3379 if isinstance(desc, basestring):
3380 lines = desc.splitlines()
3381 else:
3382 lines = [line.rstrip() for line in desc]
3383 while lines and not lines[0]:
3384 lines.pop(0)
3385 while lines and not lines[-1]:
3386 lines.pop(-1)
3387 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003388
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003389 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3390 """Rewrites the R=/TBR= line(s) as a single line each.
3391
3392 Args:
3393 reviewers (list(str)) - list of additional emails to use for reviewers.
3394 tbrs (list(str)) - list of additional emails to use for TBRs.
3395 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3396 the change that are missing OWNER coverage. If this is not None, you
3397 must also pass a value for `change`.
3398 change (Change) - The Change that should be used for OWNERS lookups.
3399 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003400 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003401 assert isinstance(tbrs, list), tbrs
3402
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003403 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003404 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003405
3406 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003407 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003408
3409 reviewers = set(reviewers)
3410 tbrs = set(tbrs)
3411 LOOKUP = {
3412 'TBR': tbrs,
3413 'R': reviewers,
3414 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003415
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003416 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003417 regexp = re.compile(self.R_LINE)
3418 matches = [regexp.match(line) for line in self._description_lines]
3419 new_desc = [l for i, l in enumerate(self._description_lines)
3420 if not matches[i]]
3421 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003422
agable@chromium.org42c20792013-09-12 17:34:49 +00003423 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003424
3425 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003426 for match in matches:
3427 if not match:
3428 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003429 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3430
3431 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003432 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003433 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003434 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003435 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003436 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003437 LOOKUP[add_owners_to].update(
3438 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003439
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003440 # If any folks ended up in both groups, remove them from tbrs.
3441 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003442
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003443 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3444 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003445
3446 # Put the new lines in the description where the old first R= line was.
3447 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3448 if 0 <= line_loc < len(self._description_lines):
3449 if new_tbr_line:
3450 self._description_lines.insert(line_loc, new_tbr_line)
3451 if new_r_line:
3452 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003453 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003454 if new_r_line:
3455 self.append_footer(new_r_line)
3456 if new_tbr_line:
3457 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003458
Aaron Gable3a16ed12017-03-23 10:51:55 -07003459 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003460 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003461 self.set_description([
3462 '# Enter a description of the change.',
3463 '# This will be displayed on the codereview site.',
3464 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003465 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003466 '--------------------',
3467 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003468
agable@chromium.org42c20792013-09-12 17:34:49 +00003469 regexp = re.compile(self.BUG_LINE)
3470 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003471 prefix = settings.GetBugPrefix()
3472 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003473 if git_footer:
3474 self.append_footer('Bug: %s' % ', '.join(values))
3475 else:
3476 for value in values:
3477 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003478
agable@chromium.org42c20792013-09-12 17:34:49 +00003479 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003480 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003481 if not content:
3482 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003483 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003484
Bruce Dawson2377b012018-01-11 16:46:49 -08003485 # Strip off comments and default inserted "Bug:" line.
3486 clean_lines = [line.rstrip() for line in lines if not
3487 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003488 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003489 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003490 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003491
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003492 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003493 """Adds a footer line to the description.
3494
3495 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3496 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3497 that Gerrit footers are always at the end.
3498 """
3499 parsed_footer_line = git_footers.parse_footer(line)
3500 if parsed_footer_line:
3501 # Line is a gerrit footer in the form: Footer-Key: any value.
3502 # Thus, must be appended observing Gerrit footer rules.
3503 self.set_description(
3504 git_footers.add_footer(self.description,
3505 key=parsed_footer_line[0],
3506 value=parsed_footer_line[1]))
3507 return
3508
3509 if not self._description_lines:
3510 self._description_lines.append(line)
3511 return
3512
3513 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3514 if gerrit_footers:
3515 # git_footers.split_footers ensures that there is an empty line before
3516 # actual (gerrit) footers, if any. We have to keep it that way.
3517 assert top_lines and top_lines[-1] == ''
3518 top_lines, separator = top_lines[:-1], top_lines[-1:]
3519 else:
3520 separator = [] # No need for separator if there are no gerrit_footers.
3521
3522 prev_line = top_lines[-1] if top_lines else ''
3523 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3524 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3525 top_lines.append('')
3526 top_lines.append(line)
3527 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003528
tandrii99a72f22016-08-17 14:33:24 -07003529 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003530 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003531 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003532 reviewers = [match.group(2).strip()
3533 for match in matches
3534 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003535 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003536
bradnelsond975b302016-10-23 12:20:23 -07003537 def get_cced(self):
3538 """Retrieves the list of reviewers."""
3539 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3540 cced = [match.group(2).strip() for match in matches if match]
3541 return cleanup_list(cced)
3542
Nodir Turakulov23b82142017-11-16 11:04:25 -08003543 def get_hash_tags(self):
3544 """Extracts and sanitizes a list of Gerrit hashtags."""
3545 subject = (self._description_lines or ('',))[0]
3546 subject = re.sub(
3547 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3548
3549 tags = []
3550 start = 0
3551 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3552 while True:
3553 m = bracket_exp.match(subject, start)
3554 if not m:
3555 break
3556 tags.append(self.sanitize_hash_tag(m.group(1)))
3557 start = m.end()
3558
3559 if not tags:
3560 # Try "Tag: " prefix.
3561 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3562 if m:
3563 tags.append(self.sanitize_hash_tag(m.group(1)))
3564 return tags
3565
3566 @classmethod
3567 def sanitize_hash_tag(cls, tag):
3568 """Returns a sanitized Gerrit hash tag.
3569
3570 A sanitized hashtag can be used as a git push refspec parameter value.
3571 """
3572 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3573
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003574 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3575 """Updates this commit description given the parent.
3576
3577 This is essentially what Gnumbd used to do.
3578 Consult https://goo.gl/WMmpDe for more details.
3579 """
3580 assert parent_msg # No, orphan branch creation isn't supported.
3581 assert parent_hash
3582 assert dest_ref
3583 parent_footer_map = git_footers.parse_footers(parent_msg)
3584 # This will also happily parse svn-position, which GnumbD is no longer
3585 # supporting. While we'd generate correct footers, the verifier plugin
3586 # installed in Gerrit will block such commit (ie git push below will fail).
3587 parent_position = git_footers.get_position(parent_footer_map)
3588
3589 # Cherry-picks may have last line obscuring their prior footers,
3590 # from git_footers perspective. This is also what Gnumbd did.
3591 cp_line = None
3592 if (self._description_lines and
3593 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3594 cp_line = self._description_lines.pop()
3595
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003596 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003597
3598 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3599 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003600 for i, line in enumerate(footer_lines):
3601 k, v = git_footers.parse_footer(line) or (None, None)
3602 if k and k.startswith('Cr-'):
3603 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003604
3605 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003606 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003607 if parent_position[0] == dest_ref:
3608 # Same branch as parent.
3609 number = int(parent_position[1]) + 1
3610 else:
3611 number = 1 # New branch, and extra lineage.
3612 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3613 int(parent_position[1])))
3614
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003615 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3616 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003617
3618 self._description_lines = top_lines
3619 if cp_line:
3620 self._description_lines.append(cp_line)
3621 if self._description_lines[-1] != '':
3622 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003623 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003624
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003625
Aaron Gablea1bab272017-04-11 16:38:18 -07003626def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003627 """Retrieves the reviewers that approved a CL from the issue properties with
3628 messages.
3629
3630 Note that the list may contain reviewers that are not committer, thus are not
3631 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003632
3633 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003634 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003635 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003636 return sorted(
3637 set(
3638 message['sender']
3639 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003640 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003641 )
3642 )
3643
3644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003645def FindCodereviewSettingsFile(filename='codereview.settings'):
3646 """Finds the given file starting in the cwd and going up.
3647
3648 Only looks up to the top of the repository unless an
3649 'inherit-review-settings-ok' file exists in the root of the repository.
3650 """
3651 inherit_ok_file = 'inherit-review-settings-ok'
3652 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003653 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003654 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3655 root = '/'
3656 while True:
3657 if filename in os.listdir(cwd):
3658 if os.path.isfile(os.path.join(cwd, filename)):
3659 return open(os.path.join(cwd, filename))
3660 if cwd == root:
3661 break
3662 cwd = os.path.dirname(cwd)
3663
3664
3665def LoadCodereviewSettingsFromFile(fileobj):
3666 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003667 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003668
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003669 def SetProperty(name, setting, unset_error_ok=False):
3670 fullname = 'rietveld.' + name
3671 if setting in keyvals:
3672 RunGit(['config', fullname, keyvals[setting]])
3673 else:
3674 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3675
tandrii48df5812016-10-17 03:55:37 -07003676 if not keyvals.get('GERRIT_HOST', False):
3677 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003678 # Only server setting is required. Other settings can be absent.
3679 # In that case, we ignore errors raised during option deletion attempt.
3680 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003681 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003682 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3683 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003684 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003685 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3686 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003687 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003688 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3689 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003690
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003691 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003692 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003693
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003694 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003695 RunGit(['config', 'gerrit.squash-uploads',
3696 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003697
tandrii@chromium.org28253532016-04-14 13:46:56 +00003698 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003699 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003700 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3701
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003702 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003703 # should be of the form
3704 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3705 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003706 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3707 keyvals['ORIGIN_URL_CONFIG']])
3708
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003709
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003710def urlretrieve(source, destination):
3711 """urllib is broken for SSL connections via a proxy therefore we
3712 can't use urllib.urlretrieve()."""
3713 with open(destination, 'w') as f:
3714 f.write(urllib2.urlopen(source).read())
3715
3716
ukai@chromium.org712d6102013-11-27 00:52:58 +00003717def hasSheBang(fname):
3718 """Checks fname is a #! script."""
3719 with open(fname) as f:
3720 return f.read(2).startswith('#!')
3721
3722
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003723# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3724def DownloadHooks(*args, **kwargs):
3725 pass
3726
3727
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003728def DownloadGerritHook(force):
3729 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003730
3731 Args:
3732 force: True to update hooks. False to install hooks if not present.
3733 """
3734 if not settings.GetIsGerrit():
3735 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003736 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003737 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3738 if not os.access(dst, os.X_OK):
3739 if os.path.exists(dst):
3740 if not force:
3741 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003742 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003743 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003744 if not hasSheBang(dst):
3745 DieWithError('Not a script: %s\n'
3746 'You need to download from\n%s\n'
3747 'into .git/hooks/commit-msg and '
3748 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003749 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3750 except Exception:
3751 if os.path.exists(dst):
3752 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003753 DieWithError('\nFailed to download hooks.\n'
3754 'You need to download from\n%s\n'
3755 'into .git/hooks/commit-msg and '
3756 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003757
3758
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003759def GetRietveldCodereviewSettingsInteractively():
3760 """Prompt the user for settings."""
3761 server = settings.GetDefaultServerUrl(error_ok=True)
3762 prompt = 'Rietveld server (host[:port])'
3763 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3764 newserver = ask_for_data(prompt + ':')
3765 if not server and not newserver:
3766 newserver = DEFAULT_SERVER
3767 if newserver:
3768 newserver = gclient_utils.UpgradeToHttps(newserver)
3769 if newserver != server:
3770 RunGit(['config', 'rietveld.server', newserver])
3771
3772 def SetProperty(initial, caption, name, is_url):
3773 prompt = caption
3774 if initial:
3775 prompt += ' ("x" to clear) [%s]' % initial
3776 new_val = ask_for_data(prompt + ':')
3777 if new_val == 'x':
3778 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3779 elif new_val:
3780 if is_url:
3781 new_val = gclient_utils.UpgradeToHttps(new_val)
3782 if new_val != initial:
3783 RunGit(['config', 'rietveld.' + name, new_val])
3784
3785 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3786 SetProperty(settings.GetDefaultPrivateFlag(),
3787 'Private flag (rietveld only)', 'private', False)
3788 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3789 'tree-status-url', False)
3790 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3791 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3792 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3793 'run-post-upload-hook', False)
3794
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003795
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003796class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003797 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003798
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003799 _GOOGLESOURCE = 'googlesource.com'
3800
3801 def __init__(self):
3802 # Cached list of [host, identity, source], where source is either
3803 # .gitcookies or .netrc.
3804 self._all_hosts = None
3805
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003806 def ensure_configured_gitcookies(self):
3807 """Runs checks and suggests fixes to make git use .gitcookies from default
3808 path."""
3809 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3810 configured_path = RunGitSilent(
3811 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003812 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003813 if configured_path:
3814 self._ensure_default_gitcookies_path(configured_path, default)
3815 else:
3816 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003817
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003818 @staticmethod
3819 def _ensure_default_gitcookies_path(configured_path, default_path):
3820 assert configured_path
3821 if configured_path == default_path:
3822 print('git is already configured to use your .gitcookies from %s' %
3823 configured_path)
3824 return
3825
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003826 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003827 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3828 (configured_path, default_path))
3829
3830 if not os.path.exists(configured_path):
3831 print('However, your configured .gitcookies file is missing.')
3832 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3833 action='reconfigure')
3834 RunGit(['config', '--global', 'http.cookiefile', default_path])
3835 return
3836
3837 if os.path.exists(default_path):
3838 print('WARNING: default .gitcookies file already exists %s' %
3839 default_path)
3840 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3841 default_path)
3842
3843 confirm_or_exit('Move existing .gitcookies to default location?',
3844 action='move')
3845 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003846 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003847 print('Moved and reconfigured git to use .gitcookies from %s' %
3848 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003849
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003850 @staticmethod
3851 def _configure_gitcookies_path(default_path):
3852 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3853 if os.path.exists(netrc_path):
3854 print('You seem to be using outdated .netrc for git credentials: %s' %
3855 netrc_path)
3856 print('This tool will guide you through setting up recommended '
3857 '.gitcookies store for git credentials.\n'
3858 '\n'
3859 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3860 ' git config --global --unset http.cookiefile\n'
3861 ' mv %s %s.backup\n\n' % (default_path, default_path))
3862 confirm_or_exit(action='setup .gitcookies')
3863 RunGit(['config', '--global', 'http.cookiefile', default_path])
3864 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003865
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003866 def get_hosts_with_creds(self, include_netrc=False):
3867 if self._all_hosts is None:
3868 a = gerrit_util.CookiesAuthenticator()
3869 self._all_hosts = [
3870 (h, u, s)
3871 for h, u, s in itertools.chain(
3872 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3873 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3874 )
3875 if h.endswith(self._GOOGLESOURCE)
3876 ]
3877
3878 if include_netrc:
3879 return self._all_hosts
3880 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3881
3882 def print_current_creds(self, include_netrc=False):
3883 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3884 if not hosts:
3885 print('No Git/Gerrit credentials found')
3886 return
3887 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3888 header = [('Host', 'User', 'Which file'),
3889 ['=' * l for l in lengths]]
3890 for row in (header + hosts):
3891 print('\t'.join((('%%+%ds' % l) % s)
3892 for l, s in zip(lengths, row)))
3893
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003894 @staticmethod
3895 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003896 """Parses identity "git-<username>.domain" into <username> and domain."""
3897 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003898 # distinguishable from sub-domains. But we do know typical domains:
3899 if identity.endswith('.chromium.org'):
3900 domain = 'chromium.org'
3901 username = identity[:-len('.chromium.org')]
3902 else:
3903 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003904 if username.startswith('git-'):
3905 username = username[len('git-'):]
3906 return username, domain
3907
3908 def _get_usernames_of_domain(self, domain):
3909 """Returns list of usernames referenced by .gitcookies in a given domain."""
3910 identities_by_domain = {}
3911 for _, identity, _ in self.get_hosts_with_creds():
3912 username, domain = self._parse_identity(identity)
3913 identities_by_domain.setdefault(domain, []).append(username)
3914 return identities_by_domain.get(domain)
3915
3916 def _canonical_git_googlesource_host(self, host):
3917 """Normalizes Gerrit hosts (with '-review') to Git host."""
3918 assert host.endswith(self._GOOGLESOURCE)
3919 # Prefix doesn't include '.' at the end.
3920 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3921 if prefix.endswith('-review'):
3922 prefix = prefix[:-len('-review')]
3923 return prefix + '.' + self._GOOGLESOURCE
3924
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003925 def _canonical_gerrit_googlesource_host(self, host):
3926 git_host = self._canonical_git_googlesource_host(host)
3927 prefix = git_host.split('.', 1)[0]
3928 return prefix + '-review.' + self._GOOGLESOURCE
3929
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003930 def _get_counterpart_host(self, host):
3931 assert host.endswith(self._GOOGLESOURCE)
3932 git = self._canonical_git_googlesource_host(host)
3933 gerrit = self._canonical_gerrit_googlesource_host(git)
3934 return git if gerrit == host else gerrit
3935
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003936 def has_generic_host(self):
3937 """Returns whether generic .googlesource.com has been configured.
3938
3939 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3940 """
3941 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3942 if host == '.' + self._GOOGLESOURCE:
3943 return True
3944 return False
3945
3946 def _get_git_gerrit_identity_pairs(self):
3947 """Returns map from canonic host to pair of identities (Git, Gerrit).
3948
3949 One of identities might be None, meaning not configured.
3950 """
3951 host_to_identity_pairs = {}
3952 for host, identity, _ in self.get_hosts_with_creds():
3953 canonical = self._canonical_git_googlesource_host(host)
3954 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3955 idx = 0 if canonical == host else 1
3956 pair[idx] = identity
3957 return host_to_identity_pairs
3958
3959 def get_partially_configured_hosts(self):
3960 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003961 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3962 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3963 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003964
3965 def get_conflicting_hosts(self):
3966 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003967 host
3968 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003969 if None not in (i1, i2) and i1 != i2)
3970
3971 def get_duplicated_hosts(self):
3972 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3973 return set(host for host, count in counters.iteritems() if count > 1)
3974
3975 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3976 'chromium.googlesource.com': 'chromium.org',
3977 'chrome-internal.googlesource.com': 'google.com',
3978 }
3979
3980 def get_hosts_with_wrong_identities(self):
3981 """Finds hosts which **likely** reference wrong identities.
3982
3983 Note: skips hosts which have conflicting identities for Git and Gerrit.
3984 """
3985 hosts = set()
3986 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3987 pair = self._get_git_gerrit_identity_pairs().get(host)
3988 if pair and pair[0] == pair[1]:
3989 _, domain = self._parse_identity(pair[0])
3990 if domain != expected:
3991 hosts.add(host)
3992 return hosts
3993
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003994 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003995 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003996 hosts = sorted(hosts)
3997 assert hosts
3998 if extra_column_func is None:
3999 extras = [''] * len(hosts)
4000 else:
4001 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004002 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
4003 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004004 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004005 lines.append(tmpl % he)
4006 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004007
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004008 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004009 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004010 yield ('.googlesource.com wildcard record detected',
4011 ['Chrome Infrastructure team recommends to list full host names '
4012 'explicitly.'],
4013 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004014
4015 dups = self.get_duplicated_hosts()
4016 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004017 yield ('The following hosts were defined twice',
4018 self._format_hosts(dups),
4019 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004020
4021 partial = self.get_partially_configured_hosts()
4022 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004023 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4024 'These hosts are missing',
4025 self._format_hosts(partial, lambda host: 'but %s defined' %
4026 self._get_counterpart_host(host)),
4027 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004028
4029 conflicting = self.get_conflicting_hosts()
4030 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004031 yield ('The following Git hosts have differing credentials from their '
4032 'Gerrit counterparts',
4033 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4034 tuple(self._get_git_gerrit_identity_pairs()[host])),
4035 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004036
4037 wrong = self.get_hosts_with_wrong_identities()
4038 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004039 yield ('These hosts likely use wrong identity',
4040 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4041 (self._get_git_gerrit_identity_pairs()[host][0],
4042 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4043 wrong)
4044
4045 def find_and_report_problems(self):
4046 """Returns True if there was at least one problem, else False."""
4047 found = False
4048 bad_hosts = set()
4049 for title, sublines, hosts in self._find_problems():
4050 if not found:
4051 found = True
4052 print('\n\n.gitcookies problem report:\n')
4053 bad_hosts.update(hosts or [])
4054 print(' %s%s' % (title , (':' if sublines else '')))
4055 if sublines:
4056 print()
4057 print(' %s' % '\n '.join(sublines))
4058 print()
4059
4060 if bad_hosts:
4061 assert found
4062 print(' You can manually remove corresponding lines in your %s file and '
4063 'visit the following URLs with correct account to generate '
4064 'correct credential lines:\n' %
4065 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4066 print(' %s' % '\n '.join(sorted(set(
4067 gerrit_util.CookiesAuthenticator().get_new_password_url(
4068 self._canonical_git_googlesource_host(host))
4069 for host in bad_hosts
4070 ))))
4071 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004072
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004073
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004074@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004075def CMDcreds_check(parser, args):
4076 """Checks credentials and suggests changes."""
4077 _, _ = parser.parse_args(args)
4078
4079 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004080 DieWithError(
4081 'This command is not designed for GCE, are you on a bot?\n'
4082 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004083
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004084 checker = _GitCookiesChecker()
4085 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004086
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004087 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004088 checker.print_current_creds(include_netrc=True)
4089
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004090 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004091 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004092 return 0
4093 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004094
4095
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004096@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004097@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004098def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004099 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004100
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004101 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004102 # TODO(tandrii): remove this once we switch to Gerrit.
4103 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004104 parser.add_option('--activate-update', action='store_true',
4105 help='activate auto-updating [rietveld] section in '
4106 '.git/config')
4107 parser.add_option('--deactivate-update', action='store_true',
4108 help='deactivate auto-updating [rietveld] section in '
4109 '.git/config')
4110 options, args = parser.parse_args(args)
4111
4112 if options.deactivate_update:
4113 RunGit(['config', 'rietveld.autoupdate', 'false'])
4114 return
4115
4116 if options.activate_update:
4117 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4118 return
4119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004120 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004121 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004122 return 0
4123
4124 url = args[0]
4125 if not url.endswith('codereview.settings'):
4126 url = os.path.join(url, 'codereview.settings')
4127
4128 # Load code review settings and download hooks (if available).
4129 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4130 return 0
4131
4132
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004133@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004134def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004135 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004136 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4137 branch = ShortBranchName(branchref)
4138 _, args = parser.parse_args(args)
4139 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004140 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004141 return RunGit(['config', 'branch.%s.base-url' % branch],
4142 error_ok=False).strip()
4143 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004144 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004145 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4146 error_ok=False).strip()
4147
4148
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004149def color_for_status(status):
4150 """Maps a Changelist status to color, for CMDstatus and other tools."""
4151 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004152 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004153 'waiting': Fore.BLUE,
4154 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004155 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004156 'lgtm': Fore.GREEN,
4157 'commit': Fore.MAGENTA,
4158 'closed': Fore.CYAN,
4159 'error': Fore.WHITE,
4160 }.get(status, Fore.WHITE)
4161
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004162
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004163def get_cl_statuses(changes, fine_grained, max_processes=None):
4164 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004165
4166 If fine_grained is true, this will fetch CL statuses from the server.
4167 Otherwise, simply indicate if there's a matching url for the given branches.
4168
4169 If max_processes is specified, it is used as the maximum number of processes
4170 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4171 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004172
4173 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004174 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004175 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004176 upload.verbosity = 0
4177
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004178 if not changes:
4179 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004180
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004181 if not fine_grained:
4182 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004183 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004184 for cl in changes:
4185 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004186 return
4187
4188 # First, sort out authentication issues.
4189 logging.debug('ensuring credentials exist')
4190 for cl in changes:
4191 cl.EnsureAuthenticated(force=False, refresh=True)
4192
4193 def fetch(cl):
4194 try:
4195 return (cl, cl.GetStatus())
4196 except:
4197 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004198 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004199 raise
4200
4201 threads_count = len(changes)
4202 if max_processes:
4203 threads_count = max(1, min(threads_count, max_processes))
4204 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4205
4206 pool = ThreadPool(threads_count)
4207 fetched_cls = set()
4208 try:
4209 it = pool.imap_unordered(fetch, changes).__iter__()
4210 while True:
4211 try:
4212 cl, status = it.next(timeout=5)
4213 except multiprocessing.TimeoutError:
4214 break
4215 fetched_cls.add(cl)
4216 yield cl, status
4217 finally:
4218 pool.close()
4219
4220 # Add any branches that failed to fetch.
4221 for cl in set(changes) - fetched_cls:
4222 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004223
rmistry@google.com2dd99862015-06-22 12:22:18 +00004224
4225def upload_branch_deps(cl, args):
4226 """Uploads CLs of local branches that are dependents of the current branch.
4227
4228 If the local branch dependency tree looks like:
4229 test1 -> test2.1 -> test3.1
4230 -> test3.2
4231 -> test2.2 -> test3.3
4232
4233 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4234 run on the dependent branches in this order:
4235 test2.1, test3.1, test3.2, test2.2, test3.3
4236
4237 Note: This function does not rebase your local dependent branches. Use it when
4238 you make a change to the parent branch that will not conflict with its
4239 dependent branches, and you would like their dependencies updated in
4240 Rietveld.
4241 """
4242 if git_common.is_dirty_git_tree('upload-branch-deps'):
4243 return 1
4244
4245 root_branch = cl.GetBranch()
4246 if root_branch is None:
4247 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4248 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004249 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004250 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4251 'patchset dependencies without an uploaded CL.')
4252
4253 branches = RunGit(['for-each-ref',
4254 '--format=%(refname:short) %(upstream:short)',
4255 'refs/heads'])
4256 if not branches:
4257 print('No local branches found.')
4258 return 0
4259
4260 # Create a dictionary of all local branches to the branches that are dependent
4261 # on it.
4262 tracked_to_dependents = collections.defaultdict(list)
4263 for b in branches.splitlines():
4264 tokens = b.split()
4265 if len(tokens) == 2:
4266 branch_name, tracked = tokens
4267 tracked_to_dependents[tracked].append(branch_name)
4268
vapiera7fbd5a2016-06-16 09:17:49 -07004269 print()
4270 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004271 dependents = []
4272 def traverse_dependents_preorder(branch, padding=''):
4273 dependents_to_process = tracked_to_dependents.get(branch, [])
4274 padding += ' '
4275 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004276 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004277 dependents.append(dependent)
4278 traverse_dependents_preorder(dependent, padding)
4279 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004280 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004281
4282 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004283 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004284 return 0
4285
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004286 confirm_or_exit('This command will checkout all dependent branches and run '
4287 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004288
andybons@chromium.org962f9462016-02-03 20:00:42 +00004289 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004290 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004291 args.extend(['-t', 'Updated patchset dependency'])
4292
rmistry@google.com2dd99862015-06-22 12:22:18 +00004293 # Record all dependents that failed to upload.
4294 failures = {}
4295 # Go through all dependents, checkout the branch and upload.
4296 try:
4297 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004298 print()
4299 print('--------------------------------------')
4300 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004301 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004302 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004303 try:
4304 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004305 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004306 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004307 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004308 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004309 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004310 finally:
4311 # Swap back to the original root branch.
4312 RunGit(['checkout', '-q', root_branch])
4313
vapiera7fbd5a2016-06-16 09:17:49 -07004314 print()
4315 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004316 for dependent_branch in dependents:
4317 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004318 print(' %s : %s' % (dependent_branch, upload_status))
4319 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004320
4321 return 0
4322
4323
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004324@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004325def CMDarchive(parser, args):
4326 """Archives and deletes branches associated with closed changelists."""
4327 parser.add_option(
4328 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004329 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004330 parser.add_option(
4331 '-f', '--force', action='store_true',
4332 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004333 parser.add_option(
4334 '-d', '--dry-run', action='store_true',
4335 help='Skip the branch tagging and removal steps.')
4336 parser.add_option(
4337 '-t', '--notags', action='store_true',
4338 help='Do not tag archived branches. '
4339 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004340
4341 auth.add_auth_options(parser)
4342 options, args = parser.parse_args(args)
4343 if args:
4344 parser.error('Unsupported args: %s' % ' '.join(args))
4345 auth_config = auth.extract_auth_config_from_options(options)
4346
4347 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4348 if not branches:
4349 return 0
4350
vapiera7fbd5a2016-06-16 09:17:49 -07004351 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004352 changes = [Changelist(branchref=b, auth_config=auth_config)
4353 for b in branches.splitlines()]
4354 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4355 statuses = get_cl_statuses(changes,
4356 fine_grained=True,
4357 max_processes=options.maxjobs)
4358 proposal = [(cl.GetBranch(),
4359 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4360 for cl, status in statuses
4361 if status == 'closed']
4362 proposal.sort()
4363
4364 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004365 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004366 return 0
4367
4368 current_branch = GetCurrentBranch()
4369
vapiera7fbd5a2016-06-16 09:17:49 -07004370 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004371 if options.notags:
4372 for next_item in proposal:
4373 print(' ' + next_item[0])
4374 else:
4375 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4376 for next_item in proposal:
4377 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004378
kmarshall9249e012016-08-23 12:02:16 -07004379 # Quit now on precondition failure or if instructed by the user, either
4380 # via an interactive prompt or by command line flags.
4381 if options.dry_run:
4382 print('\nNo changes were made (dry run).\n')
4383 return 0
4384 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004385 print('You are currently on a branch \'%s\' which is associated with a '
4386 'closed codereview issue, so archive cannot proceed. Please '
4387 'checkout another branch and run this command again.' %
4388 current_branch)
4389 return 1
kmarshall9249e012016-08-23 12:02:16 -07004390 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004391 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4392 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004393 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004394 return 1
4395
4396 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004397 if not options.notags:
4398 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004399 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004400
vapiera7fbd5a2016-06-16 09:17:49 -07004401 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004402
4403 return 0
4404
4405
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004406@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004407def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004408 """Show status of changelists.
4409
4410 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004411 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004412 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004413 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004414 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004415 - Magenta in the commit queue
4416 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004417 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004418
4419 Also see 'git cl comments'.
4420 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004421 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004422 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004423 parser.add_option('-f', '--fast', action='store_true',
4424 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004425 parser.add_option(
4426 '-j', '--maxjobs', action='store', type=int,
4427 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004428
4429 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004430 _add_codereview_issue_select_options(
4431 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004432 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004433 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004434 if args:
4435 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004436 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004437
iannuccie53c9352016-08-17 14:40:40 -07004438 if options.issue is not None and not options.field:
4439 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004440
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004441 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004442 cl = Changelist(auth_config=auth_config, issue=options.issue,
4443 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004444 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004445 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004446 elif options.field == 'id':
4447 issueid = cl.GetIssue()
4448 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004449 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004450 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004451 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004452 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004453 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004454 elif options.field == 'status':
4455 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004456 elif options.field == 'url':
4457 url = cl.GetIssueURL()
4458 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004459 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004460 return 0
4461
4462 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4463 if not branches:
4464 print('No local branch found.')
4465 return 0
4466
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004467 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004468 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004469 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004470 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004471 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004472 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004473 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004474
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004475 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004476 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4477 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4478 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004479 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004480 c, status = output.next()
4481 branch_statuses[c.GetBranch()] = status
4482 status = branch_statuses.pop(branch)
4483 url = cl.GetIssueURL()
4484 if url and (not status or status == 'error'):
4485 # The issue probably doesn't exist anymore.
4486 url += ' (broken)'
4487
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004488 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004489 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004490 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004491 color = ''
4492 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004493 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004494 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004495 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004496 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004497
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004498
4499 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004500 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004501 print('Current branch: %s' % branch)
4502 for cl in changes:
4503 if cl.GetBranch() == branch:
4504 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004505 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004506 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004507 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004508 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004509 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004510 print('Issue description:')
4511 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004512 return 0
4513
4514
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004515def colorize_CMDstatus_doc():
4516 """To be called once in main() to add colors to git cl status help."""
4517 colors = [i for i in dir(Fore) if i[0].isupper()]
4518
4519 def colorize_line(line):
4520 for color in colors:
4521 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004522 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004523 indent = len(line) - len(line.lstrip(' ')) + 1
4524 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4525 return line
4526
4527 lines = CMDstatus.__doc__.splitlines()
4528 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4529
4530
phajdan.jre328cf92016-08-22 04:12:17 -07004531def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004532 if path == '-':
4533 json.dump(contents, sys.stdout)
4534 else:
4535 with open(path, 'w') as f:
4536 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004537
4538
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004539@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004540@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004541def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004542 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004543
4544 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004545 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004546 parser.add_option('-r', '--reverse', action='store_true',
4547 help='Lookup the branch(es) for the specified issues. If '
4548 'no issues are specified, all branches with mapped '
4549 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004550 parser.add_option('--json',
4551 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004552 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004553 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004554 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004555
dnj@chromium.org406c4402015-03-03 17:22:28 +00004556 if options.reverse:
4557 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004558 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004559 # Reverse issue lookup.
4560 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004561
4562 git_config = {}
4563 for config in RunGit(['config', '--get-regexp',
4564 r'branch\..*issue']).splitlines():
4565 name, _space, val = config.partition(' ')
4566 git_config[name] = val
4567
dnj@chromium.org406c4402015-03-03 17:22:28 +00004568 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004569 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4570 config_key = _git_branch_config_key(ShortBranchName(branch),
4571 cls.IssueConfigKey())
4572 issue = git_config.get(config_key)
4573 if issue:
4574 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004575 if not args:
4576 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004577 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004578 for issue in args:
4579 if not issue:
4580 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004581 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004582 print('Branch for issue number %s: %s' % (
4583 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004584 if options.json:
4585 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004586 return 0
4587
4588 if len(args) > 0:
4589 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4590 if not issue.valid:
4591 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4592 'or no argument to list it.\n'
4593 'Maybe you want to run git cl status?')
4594 cl = Changelist(codereview=issue.codereview)
4595 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004596 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004597 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004598 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4599 if options.json:
4600 write_json(options.json, {
4601 'issue': cl.GetIssue(),
4602 'issue_url': cl.GetIssueURL(),
4603 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004604 return 0
4605
4606
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004607@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004608def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004609 """Shows or posts review comments for any changelist."""
4610 parser.add_option('-a', '--add-comment', dest='comment',
4611 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004612 parser.add_option('-i', '--issue', dest='issue',
4613 help='review issue id (defaults to current issue). '
4614 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004615 parser.add_option('-m', '--machine-readable', dest='readable',
4616 action='store_false', default=True,
4617 help='output comments in a format compatible with '
4618 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004619 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004620 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004621 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004622 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004623 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004624 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004625 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004626
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004627 issue = None
4628 if options.issue:
4629 try:
4630 issue = int(options.issue)
4631 except ValueError:
4632 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004633 if not options.forced_codereview:
4634 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004635
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004636 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004637 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004638 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004639
4640 if options.comment:
4641 cl.AddComment(options.comment)
4642 return 0
4643
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004644 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4645 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004646 for comment in summary:
4647 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004648 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004649 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004650 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004651 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004652 color = Fore.MAGENTA
4653 else:
4654 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004655 print('\n%s%s %s%s\n%s' % (
4656 color,
4657 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4658 comment.sender,
4659 Fore.RESET,
4660 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4661
smut@google.comc85ac942015-09-15 16:34:43 +00004662 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004663 def pre_serialize(c):
4664 dct = c.__dict__.copy()
4665 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4666 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004667 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004668 return 0
4669
4670
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004671@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004672@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004673def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004674 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004675 parser.add_option('-d', '--display', action='store_true',
4676 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004677 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004678 help='New description to set for this issue (- for stdin, '
4679 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004680 parser.add_option('-f', '--force', action='store_true',
4681 help='Delete any unpublished Gerrit edits for this issue '
4682 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004683
4684 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004685 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004686 options, args = parser.parse_args(args)
4687 _process_codereview_select_options(parser, options)
4688
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004689 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004690 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004691 target_issue_arg = ParseIssueNumberArgument(args[0],
4692 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004693 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004694 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004695
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004696 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004697
martiniss6eda05f2016-06-30 10:18:35 -07004698 kwargs = {
4699 'auth_config': auth_config,
4700 'codereview': options.forced_codereview,
4701 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004702 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004703 if target_issue_arg:
4704 kwargs['issue'] = target_issue_arg.issue
4705 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004706 if target_issue_arg.codereview and not options.forced_codereview:
4707 detected_codereview_from_url = True
4708 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004709
4710 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004711 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004712 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004713 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004714
4715 if detected_codereview_from_url:
4716 logging.info('canonical issue/change URL: %s (type: %s)\n',
4717 cl.GetIssueURL(), target_issue_arg.codereview)
4718
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004719 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004720
smut@google.com34fb6b12015-07-13 20:03:26 +00004721 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004722 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004723 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004724
4725 if options.new_description:
4726 text = options.new_description
4727 if text == '-':
4728 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004729 elif text == '+':
4730 base_branch = cl.GetCommonAncestorWithUpstream()
4731 change = cl.GetChange(base_branch, None, local_description=True)
4732 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004733
4734 description.set_description(text)
4735 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004736 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004737
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004738 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004739 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004740 return 0
4741
4742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004743def CreateDescriptionFromLog(args):
4744 """Pulls out the commit log to use as a base for the CL description."""
4745 log_args = []
4746 if len(args) == 1 and not args[0].endswith('.'):
4747 log_args = [args[0] + '..']
4748 elif len(args) == 1 and args[0].endswith('...'):
4749 log_args = [args[0][:-1]]
4750 elif len(args) == 2:
4751 log_args = [args[0] + '..' + args[1]]
4752 else:
4753 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004754 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004755
4756
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004757@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004758def CMDlint(parser, args):
4759 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004760 parser.add_option('--filter', action='append', metavar='-x,+y',
4761 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004762 auth.add_auth_options(parser)
4763 options, args = parser.parse_args(args)
4764 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004765
4766 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004767 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004768 try:
4769 import cpplint
4770 import cpplint_chromium
4771 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004772 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004773 return 1
4774
4775 # Change the current working directory before calling lint so that it
4776 # shows the correct base.
4777 previous_cwd = os.getcwd()
4778 os.chdir(settings.GetRoot())
4779 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004780 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004781 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4782 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004783 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004784 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004785 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004786
4787 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004788 command = args + files
4789 if options.filter:
4790 command = ['--filter=' + ','.join(options.filter)] + command
4791 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004792
4793 white_regex = re.compile(settings.GetLintRegex())
4794 black_regex = re.compile(settings.GetLintIgnoreRegex())
4795 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4796 for filename in filenames:
4797 if white_regex.match(filename):
4798 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004799 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004800 else:
4801 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4802 extra_check_functions)
4803 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004804 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004805 finally:
4806 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004807 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004808 if cpplint._cpplint_state.error_count != 0:
4809 return 1
4810 return 0
4811
4812
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004813@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004814def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004815 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004816 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004817 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004818 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004819 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004820 parser.add_option('--all', action='store_true',
4821 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004822 parser.add_option('--parallel', action='store_true',
4823 help='Run all tests specified by input_api.RunTests in all '
4824 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004825 auth.add_auth_options(parser)
4826 options, args = parser.parse_args(args)
4827 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004828
sbc@chromium.org71437c02015-04-09 19:29:40 +00004829 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004830 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004831 return 1
4832
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004833 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004834 if args:
4835 base_branch = args[0]
4836 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004837 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004838 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004839
Aaron Gable8076c282017-11-29 14:39:41 -08004840 if options.all:
4841 base_change = cl.GetChange(base_branch, None)
4842 files = [('M', f) for f in base_change.AllFiles()]
4843 change = presubmit_support.GitChange(
4844 base_change.Name(),
4845 base_change.FullDescriptionText(),
4846 base_change.RepositoryRoot(),
4847 files,
4848 base_change.issue,
4849 base_change.patchset,
4850 base_change.author_email,
4851 base_change._upstream)
4852 else:
4853 change = cl.GetChange(base_branch, None)
4854
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004855 cl.RunHook(
4856 committing=not options.upload,
4857 may_prompt=False,
4858 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004859 change=change,
4860 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004861 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004862
4863
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004864def GenerateGerritChangeId(message):
4865 """Returns Ixxxxxx...xxx change id.
4866
4867 Works the same way as
4868 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4869 but can be called on demand on all platforms.
4870
4871 The basic idea is to generate git hash of a state of the tree, original commit
4872 message, author/committer info and timestamps.
4873 """
4874 lines = []
4875 tree_hash = RunGitSilent(['write-tree'])
4876 lines.append('tree %s' % tree_hash.strip())
4877 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4878 if code == 0:
4879 lines.append('parent %s' % parent.strip())
4880 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4881 lines.append('author %s' % author.strip())
4882 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4883 lines.append('committer %s' % committer.strip())
4884 lines.append('')
4885 # Note: Gerrit's commit-hook actually cleans message of some lines and
4886 # whitespace. This code is not doing this, but it clearly won't decrease
4887 # entropy.
4888 lines.append(message)
4889 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4890 stdin='\n'.join(lines))
4891 return 'I%s' % change_hash.strip()
4892
4893
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004894def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004895 """Computes the remote branch ref to use for the CL.
4896
4897 Args:
4898 remote (str): The git remote for the CL.
4899 remote_branch (str): The git remote branch for the CL.
4900 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004901 """
4902 if not (remote and remote_branch):
4903 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004904
wittman@chromium.org455dc922015-01-26 20:15:50 +00004905 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004906 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004907 # refs, which are then translated into the remote full symbolic refs
4908 # below.
4909 if '/' not in target_branch:
4910 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4911 else:
4912 prefix_replacements = (
4913 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4914 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4915 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4916 )
4917 match = None
4918 for regex, replacement in prefix_replacements:
4919 match = re.search(regex, target_branch)
4920 if match:
4921 remote_branch = target_branch.replace(match.group(0), replacement)
4922 break
4923 if not match:
4924 # This is a branch path but not one we recognize; use as-is.
4925 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004926 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4927 # Handle the refs that need to land in different refs.
4928 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004929
wittman@chromium.org455dc922015-01-26 20:15:50 +00004930 # Create the true path to the remote branch.
4931 # Does the following translation:
4932 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4933 # * refs/remotes/origin/master -> refs/heads/master
4934 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4935 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4936 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4937 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4938 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4939 'refs/heads/')
4940 elif remote_branch.startswith('refs/remotes/branch-heads'):
4941 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004942
wittman@chromium.org455dc922015-01-26 20:15:50 +00004943 return remote_branch
4944
4945
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004946def cleanup_list(l):
4947 """Fixes a list so that comma separated items are put as individual items.
4948
4949 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4950 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4951 """
4952 items = sum((i.split(',') for i in l), [])
4953 stripped_items = (i.strip() for i in items)
4954 return sorted(filter(None, stripped_items))
4955
4956
Aaron Gable4db38df2017-11-03 14:59:07 -07004957@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004958@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004959def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004960 """Uploads the current changelist to codereview.
4961
4962 Can skip dependency patchset uploads for a branch by running:
4963 git config branch.branch_name.skip-deps-uploads True
4964 To unset run:
4965 git config --unset branch.branch_name.skip-deps-uploads
4966 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004967
4968 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4969 a bug number, this bug number is automatically populated in the CL
4970 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004971
4972 If subject contains text in square brackets or has "<text>: " prefix, such
4973 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4974 [git-cl] add support for hashtags
4975 Foo bar: implement foo
4976 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004977 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004978 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4979 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004980 parser.add_option('--bypass-watchlists', action='store_true',
4981 dest='bypass_watchlists',
4982 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004983 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004984 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004985 parser.add_option('--message', '-m', dest='message',
4986 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004987 parser.add_option('-b', '--bug',
4988 help='pre-populate the bug number(s) for this issue. '
4989 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004990 parser.add_option('--message-file', dest='message_file',
4991 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004992 parser.add_option('--title', '-t', dest='title',
4993 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004994 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004995 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004996 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004997 parser.add_option('--tbrs',
4998 action='append', default=[],
4999 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005000 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005001 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00005002 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005003 parser.add_option('--hashtag', dest='hashtags',
5004 action='append', default=[],
5005 help=('Gerrit hashtag for new CL; '
5006 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00005007 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08005008 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005009 parser.add_option('--emulate_svn_auto_props',
5010 '--emulate-svn-auto-props',
5011 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00005012 dest="emulate_svn_auto_props",
5013 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00005014 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07005015 help='tell the commit queue to commit this patchset; '
5016 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00005017 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005018 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00005019 metavar='TARGET',
5020 help='Apply CL to remote ref TARGET. ' +
5021 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005022 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005023 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00005024 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005025 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005026 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005027 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005028 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5029 const='TBR', help='add a set of OWNERS to TBR')
5030 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5031 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005032 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5033 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005034 help='Send the patchset to do a CQ dry run right after '
5035 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005036 parser.add_option('--dependencies', action='store_true',
5037 help='Uploads CLs of all the local branches that depend on '
5038 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04005039 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5040 help='Sends your change to the CQ after an approval. Only '
5041 'works on repos that have the Auto-Submit label '
5042 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04005043 parser.add_option('--parallel', action='store_true',
5044 help='Run all tests specified by input_api.RunTests in all '
5045 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005046
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005047 parser.add_option('--no-autocc', action='store_true',
5048 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005049 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005050 help='Set the review private. This implies --no-autocc.')
5051
5052 # TODO: remove Rietveld flags
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005053 parser.add_option('--email', default=None,
5054 help='email address to use to connect to Rietveld')
5055
rmistry@google.com2dd99862015-06-22 12:22:18 +00005056 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005057 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005058 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005059 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005060 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005061 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005062
sbc@chromium.org71437c02015-04-09 19:29:40 +00005063 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005064 return 1
5065
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005066 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005067 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005068 options.cc = cleanup_list(options.cc)
5069
tandriib80458a2016-06-23 12:20:07 -07005070 if options.message_file:
5071 if options.message:
5072 parser.error('only one of --message and --message-file allowed.')
5073 options.message = gclient_utils.FileRead(options.message_file)
5074 options.message_file = None
5075
tandrii4d0545a2016-07-06 03:56:49 -07005076 if options.cq_dry_run and options.use_commit_queue:
5077 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5078
Aaron Gableedbc4132017-09-11 13:22:28 -07005079 if options.use_commit_queue:
5080 options.send_mail = True
5081
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005082 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5083 settings.GetIsGerrit()
5084
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005085 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005086 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005087
5088
Francois Dorayd42c6812017-05-30 15:10:20 -04005089@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005090@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005091def CMDsplit(parser, args):
5092 """Splits a branch into smaller branches and uploads CLs.
5093
5094 Creates a branch and uploads a CL for each group of files modified in the
5095 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005096 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005097 the shared OWNERS file.
5098 """
5099 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005100 help="A text file containing a CL description in which "
5101 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005102 parser.add_option("-c", "--comment", dest="comment_file",
5103 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005104 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5105 default=False,
5106 help="List the files and reviewers for each CL that would "
5107 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00005108 parser.add_option("--cq-dry-run", action='store_true',
5109 help="If set, will do a cq dry run for each uploaded CL. "
5110 "Please be careful when doing this; more than ~10 CLs "
5111 "has the potential to overload our build "
5112 "infrastructure. Try to upload these not during high "
5113 "load times (usually 11-3 Mountain View time). Email "
5114 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005115 options, _ = parser.parse_args(args)
5116
5117 if not options.description_file:
5118 parser.error('No --description flag specified.')
5119
5120 def WrappedCMDupload(args):
5121 return CMDupload(OptionParser(), args)
5122
5123 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00005124 Changelist, WrappedCMDupload, options.dry_run,
5125 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005126
5127
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005128@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005129@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005130def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005131 """DEPRECATED: Used to commit the current changelist via git-svn."""
5132 message = ('git-cl no longer supports committing to SVN repositories via '
5133 'git-svn. You probably want to use `git cl land` instead.')
5134 print(message)
5135 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005136
5137
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005138# Two special branches used by git cl land.
5139MERGE_BRANCH = 'git-cl-commit'
5140CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5141
5142
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005143@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005144@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005145def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005146 """Commits the current changelist via git.
5147
5148 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5149 upstream and closes the issue automatically and atomically.
5150
5151 Otherwise (in case of Rietveld):
5152 Squashes branch into a single commit.
5153 Updates commit message with metadata (e.g. pointer to review).
5154 Pushes the code upstream.
5155 Updates review and closes.
5156 """
5157 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5158 help='bypass upload presubmit hook')
5159 parser.add_option('-m', dest='message',
5160 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005161 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005162 help="force yes to questions (don't prompt)")
5163 parser.add_option('-c', dest='contributor',
5164 help="external contributor for patch (appended to " +
5165 "description and used as author for git). Should be " +
5166 "formatted as 'First Last <email@example.com>'")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005167 parser.add_option('--parallel', action='store_true',
5168 help='Run all tests specified by input_api.RunTests in all '
5169 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005170 auth.add_auth_options(parser)
5171 (options, args) = parser.parse_args(args)
5172 auth_config = auth.extract_auth_config_from_options(options)
5173
5174 cl = Changelist(auth_config=auth_config)
5175
Robert Iannucci2e73d432018-03-14 01:10:47 -07005176 if not cl.IsGerrit():
5177 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005178
Robert Iannucci2e73d432018-03-14 01:10:47 -07005179 if options.message:
5180 # This could be implemented, but it requires sending a new patch to
5181 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5182 # Besides, Gerrit has the ability to change the commit message on submit
5183 # automatically, thus there is no need to support this option (so far?).
5184 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005185 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005186 parser.error(
5187 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5188 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5189 'the contributor\'s "name <email>". If you can\'t upload such a '
5190 'commit for review, contact your repository admin and request'
5191 '"Forge-Author" permission.')
5192 if not cl.GetIssue():
5193 DieWithError('You must upload the change first to Gerrit.\n'
5194 ' If you would rather have `git cl land` upload '
5195 'automatically for you, see http://crbug.com/642759')
5196 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005197 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005198
5199
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005200def PushToGitWithAutoRebase(remote, branch, original_description,
5201 git_numberer_enabled, max_attempts=3):
5202 """Pushes current HEAD commit on top of remote's branch.
5203
5204 Attempts to fetch and autorebase on push failures.
5205 Adds git number footers on the fly.
5206
5207 Returns integer code from last command.
5208 """
5209 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5210 code = 0
5211 attempts_left = max_attempts
5212 while attempts_left:
5213 attempts_left -= 1
5214 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5215
5216 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5217 # If fetch fails, retry.
5218 print('Fetching %s/%s...' % (remote, branch))
5219 code, out = RunGitWithCode(
5220 ['retry', 'fetch', remote,
5221 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5222 if code:
5223 print('Fetch failed with exit code %d.' % code)
5224 print(out.strip())
5225 continue
5226
5227 print('Cherry-picking commit on top of latest %s' % branch)
5228 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5229 suppress_stderr=True)
5230 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5231 code, out = RunGitWithCode(['cherry-pick', cherry])
5232 if code:
5233 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5234 'the following files have merge conflicts:' %
5235 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005236 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5237 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005238 print('Please rebase your patch and try again.')
5239 RunGitWithCode(['cherry-pick', '--abort'])
5240 break
5241
5242 commit_desc = ChangeDescription(original_description)
5243 if git_numberer_enabled:
5244 logging.debug('Adding git number footers')
5245 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5246 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5247 branch)
5248 # Ensure timestamps are monotonically increasing.
5249 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5250 _get_committer_timestamp('HEAD'))
5251 _git_amend_head(commit_desc.description, timestamp)
5252
5253 code, out = RunGitWithCode(
5254 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5255 print(out)
5256 if code == 0:
5257 break
5258 if IsFatalPushFailure(out):
5259 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005260 'user.email are correct and you have push access to the repo.\n'
5261 'Hint: run command below to diangose common Git/Gerrit credential '
5262 'problems:\n'
5263 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005264 break
5265 return code
5266
5267
5268def IsFatalPushFailure(push_stdout):
5269 """True if retrying push won't help."""
5270 return '(prohibited by Gerrit)' in push_stdout
5271
5272
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005273@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005274@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005275def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005276 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005277 parser.add_option('-b', dest='newbranch',
5278 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005279 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005280 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005281 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005282 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005283 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005284 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005285 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005286 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005287 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005288 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005289
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005290
5291 group = optparse.OptionGroup(
5292 parser,
5293 'Options for continuing work on the current issue uploaded from a '
5294 'different clone (e.g. different machine). Must be used independently '
5295 'from the other options. No issue number should be specified, and the '
5296 'branch must have an issue number associated with it')
5297 group.add_option('--reapply', action='store_true', dest='reapply',
5298 help='Reset the branch and reapply the issue.\n'
5299 'CAUTION: This will undo any local changes in this '
5300 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005301
5302 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005303 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005304 parser.add_option_group(group)
5305
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005306 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005307 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005308 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005309 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005310 auth_config = auth.extract_auth_config_from_options(options)
5311
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005312 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005313 if options.newbranch:
5314 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005315 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005316 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005317
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005318 cl = Changelist(auth_config=auth_config,
5319 codereview=options.forced_codereview)
5320 if not cl.GetIssue():
5321 parser.error('current branch must have an associated issue')
5322
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005323 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005324 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005325 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005326
5327 RunGit(['reset', '--hard', upstream])
5328 if options.pull:
5329 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005330
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005331 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5332 options.directory)
5333
5334 if len(args) != 1 or not args[0]:
5335 parser.error('Must specify issue number or url')
5336
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005337 target_issue_arg = ParseIssueNumberArgument(args[0],
5338 options.forced_codereview)
5339 if not target_issue_arg.valid:
5340 parser.error('invalid codereview url or CL id')
5341
5342 cl_kwargs = {
5343 'auth_config': auth_config,
5344 'codereview_host': target_issue_arg.hostname,
5345 'codereview': options.forced_codereview,
5346 }
5347 detected_codereview_from_url = False
5348 if target_issue_arg.codereview and not options.forced_codereview:
5349 detected_codereview_from_url = True
5350 cl_kwargs['codereview'] = target_issue_arg.codereview
5351 cl_kwargs['issue'] = target_issue_arg.issue
5352
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005353 # We don't want uncommitted changes mixed up with the patch.
5354 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005355 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005356
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005357 if options.newbranch:
5358 if options.force:
5359 RunGit(['branch', '-D', options.newbranch],
5360 stderr=subprocess2.PIPE, error_ok=True)
5361 RunGit(['new-branch', options.newbranch])
5362
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005363 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005364
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005365 if cl.IsGerrit():
5366 if options.reject:
5367 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005368 if options.directory:
5369 parser.error('--directory is not supported with Gerrit codereview.')
5370
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005371 if detected_codereview_from_url:
5372 print('canonical issue/change URL: %s (type: %s)\n' %
5373 (cl.GetIssueURL(), target_issue_arg.codereview))
5374
5375 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005376 options.nocommit, options.directory,
5377 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005378
5379
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005380def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005381 """Fetches the tree status and returns either 'open', 'closed',
5382 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005383 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005384 if url:
5385 status = urllib2.urlopen(url).read().lower()
5386 if status.find('closed') != -1 or status == '0':
5387 return 'closed'
5388 elif status.find('open') != -1 or status == '1':
5389 return 'open'
5390 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005391 return 'unset'
5392
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005393
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005394def GetTreeStatusReason():
5395 """Fetches the tree status from a json url and returns the message
5396 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005397 url = settings.GetTreeStatusUrl()
5398 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005399 connection = urllib2.urlopen(json_url)
5400 status = json.loads(connection.read())
5401 connection.close()
5402 return status['message']
5403
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005404
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005405@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005406def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005407 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005408 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005409 status = GetTreeStatus()
5410 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005411 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005412 return 2
5413
vapiera7fbd5a2016-06-16 09:17:49 -07005414 print('The tree is %s' % status)
5415 print()
5416 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005417 if status != 'open':
5418 return 1
5419 return 0
5420
5421
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005422@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005423def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005424 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005425 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005426 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005427 '-b', '--bot', action='append',
5428 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5429 'times to specify multiple builders. ex: '
5430 '"-b win_rel -b win_layout". See '
5431 'the try server waterfall for the builders name and the tests '
5432 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005433 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005434 '-B', '--bucket', default='',
5435 help=('Buildbucket bucket to send the try requests.'))
5436 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005437 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005438 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005439 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005440 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005441 help='Revision to use for the try job; default: the revision will '
5442 'be determined by the try recipe that builder runs, which usually '
5443 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005444 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005445 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005446 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005447 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005448 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005449 '--category', default='git_cl_try', help='Specify custom build category.')
5450 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005451 '--project',
5452 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005453 'in recipe to determine to which repository or directory to '
5454 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005455 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005456 '-p', '--property', dest='properties', action='append', default=[],
5457 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005458 'key2=value2 etc. The value will be treated as '
5459 'json if decodable, or as string otherwise. '
5460 'NOTE: using this may make your try job not usable for CQ, '
5461 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005462 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005463 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5464 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005465 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005466 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005467 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005468 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005469 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005470 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005471
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005472 if options.master and options.master.startswith('luci.'):
5473 parser.error(
5474 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005475 # Make sure that all properties are prop=value pairs.
5476 bad_params = [x for x in options.properties if '=' not in x]
5477 if bad_params:
5478 parser.error('Got properties with missing "=": %s' % bad_params)
5479
maruel@chromium.org15192402012-09-06 12:38:29 +00005480 if args:
5481 parser.error('Unknown arguments: %s' % args)
5482
Koji Ishii31c14782018-01-08 17:17:33 +09005483 cl = Changelist(auth_config=auth_config, issue=options.issue,
5484 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005485 if not cl.GetIssue():
5486 parser.error('Need to upload first')
5487
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005488 if cl.IsGerrit():
5489 # HACK: warm up Gerrit change detail cache to save on RPCs.
5490 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5491
tandriie113dfd2016-10-11 10:20:12 -07005492 error_message = cl.CannotTriggerTryJobReason()
5493 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005494 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005495
borenet6c0efe62016-10-19 08:13:29 -07005496 if options.bucket and options.master:
5497 parser.error('Only one of --bucket and --master may be used.')
5498
qyearsley1fdfcb62016-10-24 13:22:03 -07005499 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005500
qyearsleydd49f942016-10-28 11:57:22 -07005501 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5502 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005503 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005504 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005505 print('git cl try with no bots now defaults to CQ dry run.')
5506 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5507 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005508
borenet6c0efe62016-10-19 08:13:29 -07005509 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005510 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005511 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005512 'of bot requires an initial job from a parent (usually a builder). '
5513 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005514 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005515 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005516
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005517 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005518 # TODO(tandrii): Checking local patchset against remote patchset is only
5519 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5520 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005521 print('Warning: Codereview server has newer patchsets (%s) than most '
5522 'recent upload from local checkout (%s). Did a previous upload '
5523 'fail?\n'
5524 'By default, git cl try uses the latest patchset from '
5525 'codereview, continuing to use patchset %s.\n' %
5526 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005527
tandrii568043b2016-10-11 07:49:18 -07005528 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005529 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005530 except BuildbucketResponseException as ex:
5531 print('ERROR: %s' % ex)
5532 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005533 return 0
5534
5535
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005536@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005537def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005538 """Prints info about try jobs associated with current CL."""
5539 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005540 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005541 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005542 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005543 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005544 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005545 '--color', action='store_true', default=setup_color.IS_TTY,
5546 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005547 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005548 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5549 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005550 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005551 '--json', help=('Path of JSON output file to write try job results to,'
5552 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005553 parser.add_option_group(group)
5554 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005555 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005556 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005557 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005558 if args:
5559 parser.error('Unrecognized args: %s' % ' '.join(args))
5560
5561 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005562 cl = Changelist(
5563 issue=options.issue, codereview=options.forced_codereview,
5564 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005565 if not cl.GetIssue():
5566 parser.error('Need to upload first')
5567
tandrii221ab252016-10-06 08:12:04 -07005568 patchset = options.patchset
5569 if not patchset:
5570 patchset = cl.GetMostRecentPatchset()
5571 if not patchset:
5572 parser.error('Codereview doesn\'t know about issue %s. '
5573 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005574 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005575 cl.GetIssue())
5576
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005577 # TODO(tandrii): Checking local patchset against remote patchset is only
5578 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5579 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005580 print('Warning: Codereview server has newer patchsets (%s) than most '
5581 'recent upload from local checkout (%s). Did a previous upload '
5582 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005583 'By default, git cl try-results uses the latest patchset from '
5584 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005585 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005586 try:
tandrii221ab252016-10-06 08:12:04 -07005587 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005588 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005589 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005590 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005591 if options.json:
5592 write_try_results_json(options.json, jobs)
5593 else:
5594 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005595 return 0
5596
5597
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005598@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005599@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005600def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005601 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005602 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005603 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005604 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005605
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005606 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005607 if args:
5608 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005609 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005610 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005611 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005612 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005613
5614 # Clear configured merge-base, if there is one.
5615 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005616 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005617 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005618 return 0
5619
5620
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005621@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005622def CMDweb(parser, args):
5623 """Opens the current CL in the web browser."""
5624 _, args = parser.parse_args(args)
5625 if args:
5626 parser.error('Unrecognized args: %s' % ' '.join(args))
5627
5628 issue_url = Changelist().GetIssueURL()
5629 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005630 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005631 return 1
5632
5633 webbrowser.open(issue_url)
5634 return 0
5635
5636
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005637@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005638def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005639 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005640 parser.add_option('-d', '--dry-run', action='store_true',
5641 help='trigger in dry run mode')
5642 parser.add_option('-c', '--clear', action='store_true',
5643 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005644 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005645 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005646 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005647 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005648 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005649 if args:
5650 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005651 if options.dry_run and options.clear:
5652 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5653
iannuccie53c9352016-08-17 14:40:40 -07005654 cl = Changelist(auth_config=auth_config, issue=options.issue,
5655 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005656 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005657 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005658 elif options.dry_run:
5659 state = _CQState.DRY_RUN
5660 else:
5661 state = _CQState.COMMIT
5662 if not cl.GetIssue():
5663 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005664 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005665 return 0
5666
5667
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005668@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005669def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005670 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005671 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005672 auth.add_auth_options(parser)
5673 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005674 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005675 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005676 if args:
5677 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005678 cl = Changelist(auth_config=auth_config, issue=options.issue,
5679 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005680 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005681 if not cl.GetIssue():
5682 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005683 cl.CloseIssue()
5684 return 0
5685
5686
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005687@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005688def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005689 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005690 parser.add_option(
5691 '--stat',
5692 action='store_true',
5693 dest='stat',
5694 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005695 auth.add_auth_options(parser)
5696 options, args = parser.parse_args(args)
5697 auth_config = auth.extract_auth_config_from_options(options)
5698 if args:
5699 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005700
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005701 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005702 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005703 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005704 if not issue:
5705 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005706
Aaron Gablea718c3e2017-08-28 17:47:28 -07005707 base = cl._GitGetBranchConfigValue('last-upload-hash')
5708 if not base:
5709 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5710 if not base:
5711 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5712 revision_info = detail['revisions'][detail['current_revision']]
5713 fetch_info = revision_info['fetch']['http']
5714 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5715 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005716
Aaron Gablea718c3e2017-08-28 17:47:28 -07005717 cmd = ['git', 'diff']
5718 if options.stat:
5719 cmd.append('--stat')
5720 cmd.append(base)
5721 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005722
5723 return 0
5724
5725
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005726@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005727def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005728 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005729 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005730 '--ignore-current',
5731 action='store_true',
5732 help='Ignore the CL\'s current reviewers and start from scratch.')
5733 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005734 '--no-color',
5735 action='store_true',
5736 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005737 parser.add_option(
5738 '--batch',
5739 action='store_true',
5740 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005741 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005742 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005743 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005744
5745 author = RunGit(['config', 'user.email']).strip() or None
5746
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005747 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005748
5749 if args:
5750 if len(args) > 1:
5751 parser.error('Unknown args')
5752 base_branch = args[0]
5753 else:
5754 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005755 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005756
5757 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005758 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5759
5760 if options.batch:
5761 db = owners.Database(change.RepositoryRoot(), file, os.path)
5762 print('\n'.join(db.reviewers_for(affected_files, author)))
5763 return 0
5764
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005765 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005766 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005767 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005768 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005769 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005770 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005771 disable_color=options.no_color,
5772 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005773
5774
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005775def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005776 """Generates a diff command."""
5777 # Generate diff for the current branch's changes.
Aaron Gablef4068aa2017-12-12 15:14:09 -08005778 diff_cmd = ['-c', 'core.quotePath=false', 'diff',
5779 '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005780 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005781
5782 if args:
5783 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005784 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005785 diff_cmd.append(arg)
5786 else:
5787 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005788
5789 return diff_cmd
5790
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005791
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005792def MatchingFileType(file_name, extensions):
5793 """Returns true if the file name ends with one of the given extensions."""
5794 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005795
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005796
enne@chromium.org555cfe42014-01-29 18:21:39 +00005797@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005798@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005799def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005800 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005801 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005802 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005803 parser.add_option('--full', action='store_true',
5804 help='Reformat the full content of all touched files')
5805 parser.add_option('--dry-run', action='store_true',
5806 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005807 parser.add_option('--python', action='store_true',
5808 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005809 parser.add_option('--js', action='store_true',
5810 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005811 parser.add_option('--diff', action='store_true',
5812 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005813 parser.add_option('--presubmit', action='store_true',
5814 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005815 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005816
Daniel Chengc55eecf2016-12-30 03:11:02 -08005817 # Normalize any remaining args against the current path, so paths relative to
5818 # the current directory are still resolved as expected.
5819 args = [os.path.join(os.getcwd(), arg) for arg in args]
5820
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005821 # git diff generates paths against the root of the repository. Change
5822 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005823 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005824 if rel_base_path:
5825 os.chdir(rel_base_path)
5826
digit@chromium.org29e47272013-05-17 17:01:46 +00005827 # Grab the merge-base commit, i.e. the upstream commit of the current
5828 # branch when it was created or the last time it was rebased. This is
5829 # to cover the case where the user may have called "git fetch origin",
5830 # moving the origin branch to a newer commit, but hasn't rebased yet.
5831 upstream_commit = None
5832 cl = Changelist()
5833 upstream_branch = cl.GetUpstreamBranch()
5834 if upstream_branch:
5835 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5836 upstream_commit = upstream_commit.strip()
5837
5838 if not upstream_commit:
5839 DieWithError('Could not find base commit for this branch. '
5840 'Are you in detached state?')
5841
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005842 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5843 diff_output = RunGit(changed_files_cmd)
5844 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005845 # Filter out files deleted by this CL
5846 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005847
Christopher Lamc5ba6922017-01-24 11:19:14 +11005848 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005849 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005850
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005851 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5852 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5853 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005854 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005855
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005856 top_dir = os.path.normpath(
5857 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5858
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005859 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5860 # formatted. This is used to block during the presubmit.
5861 return_value = 0
5862
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005863 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005864 # Locate the clang-format binary in the checkout
5865 try:
5866 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005867 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005868 DieWithError(e)
5869
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005870 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005871 cmd = [clang_format_tool]
5872 if not opts.dry_run and not opts.diff:
5873 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005874 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005875 if opts.diff:
5876 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005877 else:
5878 env = os.environ.copy()
5879 env['PATH'] = str(os.path.dirname(clang_format_tool))
5880 try:
5881 script = clang_format.FindClangFormatScriptInChromiumTree(
5882 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005883 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005884 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005885
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005886 cmd = [sys.executable, script, '-p0']
5887 if not opts.dry_run and not opts.diff:
5888 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005889
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005890 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5891 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005892
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005893 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5894 if opts.diff:
5895 sys.stdout.write(stdout)
5896 if opts.dry_run and len(stdout) > 0:
5897 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005898
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005899 # Similar code to above, but using yapf on .py files rather than clang-format
5900 # on C/C++ files
5901 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005902 yapf_tool = gclient_utils.FindExecutable('yapf')
5903 if yapf_tool is None:
5904 DieWithError('yapf not found in PATH')
5905
5906 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005907 if python_diff_files:
Nodir Turakulovaf43f402018-05-31 14:54:24 -07005908 if opts.dry_run or opts.diff:
5909 cmd = [yapf_tool, '--diff'] + python_diff_files
5910 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5911 if opts.diff:
5912 sys.stdout.write(stdout)
5913 elif len(stdout) > 0:
5914 return_value = 2
5915 else:
5916 RunCommand([yapf_tool, '-i'] + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005917 else:
5918 # TODO(sbc): yapf --lines mode still has some issues.
5919 # https://github.com/google/yapf/issues/154
5920 DieWithError('--python currently only works with --full')
5921
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005922 # Dart's formatter does not have the nice property of only operating on
5923 # modified chunks, so hard code full.
5924 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005925 try:
5926 command = [dart_format.FindDartFmtToolInChromiumTree()]
5927 if not opts.dry_run and not opts.diff:
5928 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005929 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005930
ppi@chromium.org6593d932016-03-03 15:41:15 +00005931 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005932 if opts.dry_run and stdout:
5933 return_value = 2
5934 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005935 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5936 'found in this checkout. Files in other languages are still '
5937 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005938
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005939 # Format GN build files. Always run on full build files for canonical form.
5940 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005941 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005942 if opts.dry_run or opts.diff:
5943 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005944 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005945 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5946 shell=sys.platform == 'win32',
5947 cwd=top_dir)
5948 if opts.dry_run and gn_ret == 2:
5949 return_value = 2 # Not formatted.
5950 elif opts.diff and gn_ret == 2:
5951 # TODO this should compute and print the actual diff.
5952 print("This change has GN build file diff for " + gn_diff_file)
5953 elif gn_ret != 0:
5954 # For non-dry run cases (and non-2 return values for dry-run), a
5955 # nonzero error code indicates a failure, probably because the file
5956 # doesn't parse.
5957 DieWithError("gn format failed on " + gn_diff_file +
5958 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005959
Ilya Shermane081cbe2017-08-15 17:51:04 -07005960 # Skip the metrics formatting from the global presubmit hook. These files have
5961 # a separate presubmit hook that issues an error if the files need formatting,
5962 # whereas the top-level presubmit script merely issues a warning. Formatting
5963 # these files is somewhat slow, so it's important not to duplicate the work.
5964 if not opts.presubmit:
5965 for xml_dir in GetDirtyMetricsDirs(diff_files):
5966 tool_dir = os.path.join(top_dir, xml_dir)
5967 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5968 if opts.dry_run or opts.diff:
5969 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005970 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005971 if opts.diff:
5972 sys.stdout.write(stdout)
5973 if opts.dry_run and stdout:
5974 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005975
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005976 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005977
Steven Holte2e664bf2017-04-21 13:10:47 -07005978def GetDirtyMetricsDirs(diff_files):
5979 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5980 metrics_xml_dirs = [
5981 os.path.join('tools', 'metrics', 'actions'),
5982 os.path.join('tools', 'metrics', 'histograms'),
5983 os.path.join('tools', 'metrics', 'rappor'),
5984 os.path.join('tools', 'metrics', 'ukm')]
5985 for xml_dir in metrics_xml_dirs:
5986 if any(file.startswith(xml_dir) for file in xml_diff_files):
5987 yield xml_dir
5988
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005989
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005990@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005991@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005992def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005993 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005994 _, args = parser.parse_args(args)
5995
5996 if len(args) != 1:
5997 parser.print_help()
5998 return 1
5999
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006000 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006001 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006002 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006003
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006004 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006005
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006006 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006007 output = RunGit(['config', '--local', '--get-regexp',
6008 r'branch\..*\.%s' % issueprefix],
6009 error_ok=True)
6010 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006011 if issue == target_issue:
6012 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006013
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006014 branches = []
6015 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006016 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006017 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006018 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006019 return 1
6020 if len(branches) == 1:
6021 RunGit(['checkout', branches[0]])
6022 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006023 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006024 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006025 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006026 which = raw_input('Choose by index: ')
6027 try:
6028 RunGit(['checkout', branches[int(which)]])
6029 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006030 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006031 return 1
6032
6033 return 0
6034
6035
maruel@chromium.org29404b52014-09-08 22:58:00 +00006036def CMDlol(parser, args):
6037 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006038 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006039 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6040 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6041 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006042 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006043 return 0
6044
6045
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006046class OptionParser(optparse.OptionParser):
6047 """Creates the option parse and add --verbose support."""
6048 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006049 optparse.OptionParser.__init__(
6050 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006051 self.add_option(
6052 '-v', '--verbose', action='count', default=0,
6053 help='Use 2 times for more debugging info')
6054
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006055 def parse_args(self, args=None, _values=None):
6056 # Create an optparse.Values object that will store only the actual passed
6057 # options, without the defaults.
6058 actual_options = optparse.Values()
6059 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6060 # Create an optparse.Values object with the default options.
6061 options = optparse.Values(self.get_default_values().__dict__)
6062 # Update it with the options passed by the user.
6063 options._update_careful(actual_options.__dict__)
6064 # Store the options passed by the user in an _actual_options attribute.
6065 # We store only the keys, and not the values, since the values can contain
6066 # arbitrary information, which might be PII.
6067 metrics.collector.add('arguments', actual_options.__dict__.keys())
6068
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006069 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006070 logging.basicConfig(
6071 level=levels[min(options.verbose, len(levels) - 1)],
6072 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6073 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006074 return options, args
6075
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006077def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006078 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006079 print('\nYour python version %s is unsupported, please upgrade.\n' %
6080 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006081 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006082
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006083 # Reload settings.
6084 global settings
6085 settings = Settings()
6086
Edward Lemurad463c92018-07-25 21:31:23 +00006087 if not metrics.DISABLE_METRICS_COLLECTION:
6088 metrics.collector.add('project_urls', [settings.GetViewVCUrl().strip('/+')])
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006089 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006090 dispatcher = subcommand.CommandDispatcher(__name__)
6091 try:
6092 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006093 except auth.AuthenticationError as e:
6094 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006095 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006096 if e.code != 500:
6097 raise
6098 DieWithError(
6099 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6100 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006101 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006102
6103
6104if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006105 # These affect sys.stdout so do it outside of main() to simplify mocks in
6106 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006107 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006108 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00006109 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00006110 sys.exit(main(sys.argv[1:]))