blob: b9d8acf11d967829aaa2b21a43929e0355ce8764 [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)
110 print('\nError after CL description prompt -- saving description to %s\n' %
111 backup_path)
112 backup_file = open(backup_path, 'w')
113 backup_file.write(change_desc.description)
114 backup_file.close()
115
116
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000117def GetNoGitPagerEnv():
118 env = os.environ.copy()
119 # 'cat' is a magical git string that disables pagers on all platforms.
120 env['GIT_PAGER'] = 'cat'
121 return env
122
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000123
bsep@chromium.org627d9002016-04-29 00:00:52 +0000124def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000125 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000126 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000127 except subprocess2.CalledProcessError as e:
128 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000129 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000131 'Command "%s" failed.\n%s' % (
132 ' '.join(args), error_message or e.stdout or ''))
133 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000134
135
136def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000137 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000138 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000139
140
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000141def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000142 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700143 if suppress_stderr:
144 stderr = subprocess2.VOID
145 else:
146 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000147 try:
tandrii5d48c322016-08-18 16:19:37 -0700148 (out, _), code = subprocess2.communicate(['git'] + args,
149 env=GetNoGitPagerEnv(),
150 stdout=subprocess2.PIPE,
151 stderr=stderr)
152 return code, out
153 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900154 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700155 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000156
157
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000158def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000159 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000160 return RunGitWithCode(args, suppress_stderr=True)[1]
161
162
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000163def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000164 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000165 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000166 return (version.startswith(prefix) and
167 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000168
169
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000170def BranchExists(branch):
171 """Return True if specified branch exists."""
172 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
173 suppress_stderr=True)
174 return not code
175
176
tandrii2a16b952016-10-19 07:09:44 -0700177def time_sleep(seconds):
178 # Use this so that it can be mocked in tests without interfering with python
179 # system machinery.
180 import time # Local import to discourage others from importing time globally.
181 return time.sleep(seconds)
182
183
maruel@chromium.org90541732011-04-01 17:54:18 +0000184def ask_for_data(prompt):
185 try:
186 return raw_input(prompt)
187 except KeyboardInterrupt:
188 # Hide the exception.
189 sys.exit(1)
190
191
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100192def confirm_or_exit(prefix='', action='confirm'):
193 """Asks user to press enter to continue or press Ctrl+C to abort."""
194 if not prefix or prefix.endswith('\n'):
195 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100196 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100197 mid = ' Press'
198 elif prefix.endswith(' '):
199 mid = 'press'
200 else:
201 mid = ' press'
202 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
203
204
205def ask_for_explicit_yes(prompt):
206 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
207 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
208 while True:
209 if 'yes'.startswith(result):
210 return True
211 if 'no'.startswith(result):
212 return False
213 result = ask_for_data('Please, type yes or no: ').lower()
214
215
tandrii5d48c322016-08-18 16:19:37 -0700216def _git_branch_config_key(branch, key):
217 """Helper method to return Git config key for a branch."""
218 assert branch, 'branch name is required to set git config for it'
219 return 'branch.%s.%s' % (branch, key)
220
221
222def _git_get_branch_config_value(key, default=None, value_type=str,
223 branch=False):
224 """Returns git config value of given or current branch if any.
225
226 Returns default in all other cases.
227 """
228 assert value_type in (int, str, bool)
229 if branch is False: # Distinguishing default arg value from None.
230 branch = GetCurrentBranch()
231
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000232 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700233 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000234
tandrii5d48c322016-08-18 16:19:37 -0700235 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700236 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700237 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700238 # git config also has --int, but apparently git config suffers from integer
239 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700240 args.append(_git_branch_config_key(branch, key))
241 code, out = RunGitWithCode(args)
242 if code == 0:
243 value = out.strip()
244 if value_type == int:
245 return int(value)
246 if value_type == bool:
247 return bool(value.lower() == 'true')
248 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000249 return default
250
251
tandrii5d48c322016-08-18 16:19:37 -0700252def _git_set_branch_config_value(key, value, branch=None, **kwargs):
253 """Sets the value or unsets if it's None of a git branch config.
254
255 Valid, though not necessarily existing, branch must be provided,
256 otherwise currently checked out branch is used.
257 """
258 if not branch:
259 branch = GetCurrentBranch()
260 assert branch, 'a branch name OR currently checked out branch is required'
261 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700262 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700263 if value is None:
264 args.append('--unset')
265 elif isinstance(value, bool):
266 args.append('--bool')
267 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700268 else:
tandrii33a46ff2016-08-23 05:53:40 -0700269 # git config also has --int, but apparently git config suffers from integer
270 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700271 value = str(value)
272 args.append(_git_branch_config_key(branch, key))
273 if value is not None:
274 args.append(value)
275 RunGit(args, **kwargs)
276
277
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100278def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700279 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100280
281 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
282 """
283 # Git also stores timezone offset, but it only affects visual display,
284 # actual point in time is defined by this timestamp only.
285 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
286
287
288def _git_amend_head(message, committer_timestamp):
289 """Amends commit with new message and desired committer_timestamp.
290
291 Sets committer timezone to UTC.
292 """
293 env = os.environ.copy()
294 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
295 return RunGit(['commit', '--amend', '-m', message], env=env)
296
297
machenbach@chromium.org45453142015-09-15 08:45:22 +0000298def _get_properties_from_options(options):
299 properties = dict(x.split('=', 1) for x in options.properties)
300 for key, val in properties.iteritems():
301 try:
302 properties[key] = json.loads(val)
303 except ValueError:
304 pass # If a value couldn't be evaluated, treat it as a string.
305 return properties
306
307
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000308def _prefix_master(master):
309 """Convert user-specified master name to full master name.
310
311 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
312 name, while the developers always use shortened master name
313 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
314 function does the conversion for buildbucket migration.
315 """
borenet6c0efe62016-10-19 08:13:29 -0700316 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000317 return master
borenet6c0efe62016-10-19 08:13:29 -0700318 return '%s%s' % (MASTER_PREFIX, master)
319
320
321def _unprefix_master(bucket):
322 """Convert bucket name to shortened master name.
323
324 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
325 name, while the developers always use shortened master name
326 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
327 function does the conversion for buildbucket migration.
328 """
329 if bucket.startswith(MASTER_PREFIX):
330 return bucket[len(MASTER_PREFIX):]
331 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000332
333
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000334def _buildbucket_retry(operation_name, http, *args, **kwargs):
335 """Retries requests to buildbucket service and returns parsed json content."""
336 try_count = 0
337 while True:
338 response, content = http.request(*args, **kwargs)
339 try:
340 content_json = json.loads(content)
341 except ValueError:
342 content_json = None
343
344 # Buildbucket could return an error even if status==200.
345 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000346 error = content_json.get('error')
347 if error.get('code') == 403:
348 raise BuildbucketResponseException(
349 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000351 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000352 raise BuildbucketResponseException(msg)
353
354 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700355 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000356 raise BuildbucketResponseException(
357 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700358 'Please file bugs at http://crbug.com, '
359 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000360 content)
361 return content_json
362 if response.status < 500 or try_count >= 2:
363 raise httplib2.HttpLib2Error(content)
364
365 # status >= 500 means transient failures.
366 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700367 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000368 try_count += 1
369 assert False, 'unreachable'
370
371
qyearsley1fdfcb62016-10-24 13:22:03 -0700372def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700373 """Returns a dict mapping bucket names to builders and tests,
374 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700375 """
qyearsleydd49f942016-10-28 11:57:22 -0700376 # If no bots are listed, we try to get a set of builders and tests based
377 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700378 if not options.bot:
379 change = changelist.GetChange(
380 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700381 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700382 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700383 change=change,
384 changed_files=change.LocalPaths(),
385 repository_root=settings.GetRoot(),
386 default_presubmit=None,
387 project=None,
388 verbose=options.verbose,
389 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700390 if masters is None:
391 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100392 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700393
qyearsley1fdfcb62016-10-24 13:22:03 -0700394 if options.bucket:
395 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700396 if options.master:
397 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700398
qyearsleydd49f942016-10-28 11:57:22 -0700399 # If bots are listed but no master or bucket, then we need to find out
400 # the corresponding master for each bot.
401 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
402 if error_message:
403 option_parser.error(
404 'Tryserver master cannot be found because: %s\n'
405 'Please manually specify the tryserver master, e.g. '
406 '"-m tryserver.chromium.linux".' % error_message)
407 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700408
409
qyearsley123a4682016-10-26 09:12:17 -0700410def _get_bucket_map_for_builders(builders):
411 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700412 map_url = 'https://builders-map.appspot.com/'
413 try:
qyearsley123a4682016-10-26 09:12:17 -0700414 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700415 except urllib2.URLError as e:
416 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
417 (map_url, e))
418 except ValueError as e:
419 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700420 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700421 return None, 'Failed to build master map.'
422
qyearsley123a4682016-10-26 09:12:17 -0700423 bucket_map = {}
424 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800425 bucket = builders_map.get(builder, {}).get('bucket')
426 if bucket:
427 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700428 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700429
430
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800431def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700432 """Sends a request to Buildbucket to trigger try jobs for a changelist.
433
434 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700435 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700436 changelist: Changelist that the try jobs are associated with.
437 buckets: A nested dict mapping bucket names to builders to tests.
438 options: Command-line options.
439 """
tandriide281ae2016-10-12 06:02:30 -0700440 assert changelist.GetIssue(), 'CL must be uploaded first'
441 codereview_url = changelist.GetCodereviewServer()
442 assert codereview_url, 'CL must be uploaded first'
443 patchset = patchset or changelist.GetMostRecentPatchset()
444 assert patchset, 'CL must be uploaded first'
445
446 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700447 # Cache the buildbucket credentials under the codereview host key, so that
448 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700449 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000450 http = authenticator.authorize(httplib2.Http())
451 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700452
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000453 buildbucket_put_url = (
454 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000455 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700456 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
457 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
458 hostname=codereview_host,
459 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000460 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700461
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700462 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800463 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700464 if options.clobber:
465 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700466 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700467 if extra_properties:
468 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000469
470 batch_req_body = {'builds': []}
471 print_text = []
472 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700473 for bucket, builders_and_tests in sorted(buckets.iteritems()):
474 print_text.append('Bucket: %s' % bucket)
475 master = None
476 if bucket.startswith(MASTER_PREFIX):
477 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000478 for builder, tests in sorted(builders_and_tests.iteritems()):
479 print_text.append(' %s: %s' % (builder, tests))
480 parameters = {
481 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000482 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100483 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000484 'revision': options.revision,
485 }],
tandrii8c5a3532016-11-04 07:52:02 -0700486 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000487 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000488 if 'presubmit' in builder.lower():
489 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000490 if tests:
491 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700492
493 tags = [
494 'builder:%s' % builder,
495 'buildset:%s' % buildset,
496 'user_agent:git_cl_try',
497 ]
498 if master:
499 parameters['properties']['master'] = master
500 tags.append('master:%s' % master)
501
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000502 batch_req_body['builds'].append(
503 {
504 'bucket': bucket,
505 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000506 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700507 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000508 }
509 )
510
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700512 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000513 http,
514 buildbucket_put_url,
515 'PUT',
516 body=json.dumps(batch_req_body),
517 headers={'Content-Type': 'application/json'}
518 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000519 print_text.append('To see results here, run: git cl try-results')
520 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700521 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000522
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000523
tandrii221ab252016-10-06 08:12:04 -0700524def fetch_try_jobs(auth_config, changelist, buildbucket_host,
525 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700526 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000527
qyearsley53f48a12016-09-01 10:45:13 -0700528 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000529 """
tandrii221ab252016-10-06 08:12:04 -0700530 assert buildbucket_host
531 assert changelist.GetIssue(), 'CL must be uploaded first'
532 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
533 patchset = patchset or changelist.GetMostRecentPatchset()
534 assert patchset, 'CL must be uploaded first'
535
536 codereview_url = changelist.GetCodereviewServer()
537 codereview_host = urlparse.urlparse(codereview_url).hostname
538 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000539 if authenticator.has_cached_credentials():
540 http = authenticator.authorize(httplib2.Http())
541 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700542 print('Warning: Some results might be missing because %s' %
543 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700544 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000545 http = httplib2.Http()
546
547 http.force_exception_to_status_code = True
548
tandrii221ab252016-10-06 08:12:04 -0700549 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
550 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
551 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700553 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000554 params = {'tag': 'buildset:%s' % buildset}
555
556 builds = {}
557 while True:
558 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700559 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700561 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562 for build in content.get('builds', []):
563 builds[build['id']] = build
564 if 'next_cursor' in content:
565 params['start_cursor'] = content['next_cursor']
566 else:
567 break
568 return builds
569
570
qyearsleyeab3c042016-08-24 09:18:28 -0700571def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572 """Prints nicely result of fetch_try_jobs."""
573 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700574 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000575 return
576
577 # Make a copy, because we'll be modifying builds dictionary.
578 builds = builds.copy()
579 builder_names_cache = {}
580
581 def get_builder(b):
582 try:
583 return builder_names_cache[b['id']]
584 except KeyError:
585 try:
586 parameters = json.loads(b['parameters_json'])
587 name = parameters['builder_name']
588 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700589 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700590 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 name = None
592 builder_names_cache[b['id']] = name
593 return name
594
595 def get_bucket(b):
596 bucket = b['bucket']
597 if bucket.startswith('master.'):
598 return bucket[len('master.'):]
599 return bucket
600
601 if options.print_master:
602 name_fmt = '%%-%ds %%-%ds' % (
603 max(len(str(get_bucket(b))) for b in builds.itervalues()),
604 max(len(str(get_builder(b))) for b in builds.itervalues()))
605 def get_name(b):
606 return name_fmt % (get_bucket(b), get_builder(b))
607 else:
608 name_fmt = '%%-%ds' % (
609 max(len(str(get_builder(b))) for b in builds.itervalues()))
610 def get_name(b):
611 return name_fmt % get_builder(b)
612
613 def sort_key(b):
614 return b['status'], b.get('result'), get_name(b), b.get('url')
615
616 def pop(title, f, color=None, **kwargs):
617 """Pop matching builds from `builds` dict and print them."""
618
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000619 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000620 colorize = str
621 else:
622 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
623
624 result = []
625 for b in builds.values():
626 if all(b.get(k) == v for k, v in kwargs.iteritems()):
627 builds.pop(b['id'])
628 result.append(b)
629 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700630 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000631 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700632 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000633
634 total = len(builds)
635 pop(status='COMPLETED', result='SUCCESS',
636 title='Successes:', color=Fore.GREEN,
637 f=lambda b: (get_name(b), b.get('url')))
638 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
639 title='Infra Failures:', color=Fore.MAGENTA,
640 f=lambda b: (get_name(b), b.get('url')))
641 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
642 title='Failures:', color=Fore.RED,
643 f=lambda b: (get_name(b), b.get('url')))
644 pop(status='COMPLETED', result='CANCELED',
645 title='Canceled:', color=Fore.MAGENTA,
646 f=lambda b: (get_name(b),))
647 pop(status='COMPLETED', result='FAILURE',
648 failure_reason='INVALID_BUILD_DEFINITION',
649 title='Wrong master/builder name:', color=Fore.MAGENTA,
650 f=lambda b: (get_name(b),))
651 pop(status='COMPLETED', result='FAILURE',
652 title='Other failures:',
653 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
654 pop(status='COMPLETED',
655 title='Other finished:',
656 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
657 pop(status='STARTED',
658 title='Started:', color=Fore.YELLOW,
659 f=lambda b: (get_name(b), b.get('url')))
660 pop(status='SCHEDULED',
661 title='Scheduled:',
662 f=lambda b: (get_name(b), 'id=%s' % b['id']))
663 # The last section is just in case buildbucket API changes OR there is a bug.
664 pop(title='Other:',
665 f=lambda b: (get_name(b), 'id=%s' % b['id']))
666 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700667 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000668
669
qyearsley53f48a12016-09-01 10:45:13 -0700670def write_try_results_json(output_file, builds):
671 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
672
673 The input |builds| dict is assumed to be generated by Buildbucket.
674 Buildbucket documentation: http://goo.gl/G0s101
675 """
676
677 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800678 """Extracts some of the information from one build dict."""
679 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700680 return {
681 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700682 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800683 'builder_name': parameters.get('builder_name'),
684 'created_ts': build.get('created_ts'),
685 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700686 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800687 'result': build.get('result'),
688 'status': build.get('status'),
689 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700690 'url': build.get('url'),
691 }
692
693 converted = []
694 for _, build in sorted(builds.items()):
695 converted.append(convert_build_dict(build))
696 write_json(output_file, converted)
697
698
Aaron Gable13101a62018-02-09 13:20:41 -0800699def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000700 """Prints statistics about the change to the user."""
701 # --no-ext-diff is broken in some versions of Git, so try to work around
702 # this by overriding the environment (but there is still a problem if the
703 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000704 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000705 if 'GIT_EXTERNAL_DIFF' in env:
706 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000707
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000708 try:
709 stdout = sys.stdout.fileno()
710 except AttributeError:
711 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000712 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800713 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000714 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000715
716
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000717class BuildbucketResponseException(Exception):
718 pass
719
720
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721class Settings(object):
722 def __init__(self):
723 self.default_server = None
724 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000725 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000726 self.tree_status_url = None
727 self.viewvc_url = None
728 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000729 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000730 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000731 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000732 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000733 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000734 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000735
736 def LazyUpdateIfNeeded(self):
737 """Updates the settings from a codereview.settings file, if available."""
738 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000739 # The only value that actually changes the behavior is
740 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000741 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000742 error_ok=True
743 ).strip().lower()
744
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000746 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747 LoadCodereviewSettingsFromFile(cr_settings_file)
748 self.updated = True
749
750 def GetDefaultServerUrl(self, error_ok=False):
751 if not self.default_server:
752 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000753 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000754 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755 if error_ok:
756 return self.default_server
757 if not self.default_server:
758 error_message = ('Could not find settings file. You must configure '
759 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000760 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000761 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762 return self.default_server
763
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000764 @staticmethod
765 def GetRelativeRoot():
766 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000769 if self.root is None:
770 self.root = os.path.abspath(self.GetRelativeRoot())
771 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000772
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000773 def GetGitMirror(self, remote='origin'):
774 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000775 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000776 if not os.path.isdir(local_url):
777 return None
778 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
779 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100780 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100781 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000782 if mirror.exists():
783 return mirror
784 return None
785
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786 def GetTreeStatusUrl(self, error_ok=False):
787 if not self.tree_status_url:
788 error_message = ('You must configure your tree status URL by running '
789 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000790 self.tree_status_url = self._GetRietveldConfig(
791 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792 return self.tree_status_url
793
794 def GetViewVCUrl(self):
795 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000796 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 return self.viewvc_url
798
rmistry@google.com90752582014-01-14 21:04:50 +0000799 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000800 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000801
rmistry@google.com78948ed2015-07-08 23:09:57 +0000802 def GetIsSkipDependencyUpload(self, branch_name):
803 """Returns true if specified branch should skip dep uploads."""
804 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
805 error_ok=True)
806
rmistry@google.com5626a922015-02-26 14:03:30 +0000807 def GetRunPostUploadHook(self):
808 run_post_upload_hook = self._GetRietveldConfig(
809 'run-post-upload-hook', error_ok=True)
810 return run_post_upload_hook == "True"
811
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000812 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000813 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000814
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000815 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000816 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000817
ukai@chromium.orge8077812012-02-03 03:41:46 +0000818 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700819 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000820 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700821 self.is_gerrit = (
822 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000823 return self.is_gerrit
824
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000825 def GetSquashGerritUploads(self):
826 """Return true if uploads to Gerrit should be squashed by default."""
827 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700828 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
829 if self.squash_gerrit_uploads is None:
830 # Default is squash now (http://crbug.com/611892#c23).
831 self.squash_gerrit_uploads = not (
832 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
833 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000834 return self.squash_gerrit_uploads
835
tandriia60502f2016-06-20 02:01:53 -0700836 def GetSquashGerritUploadsOverride(self):
837 """Return True or False if codereview.settings should be overridden.
838
839 Returns None if no override has been defined.
840 """
841 # See also http://crbug.com/611892#c23
842 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
843 error_ok=True).strip()
844 if result == 'true':
845 return True
846 if result == 'false':
847 return False
848 return None
849
tandrii@chromium.org28253532016-04-14 13:46:56 +0000850 def GetGerritSkipEnsureAuthenticated(self):
851 """Return True if EnsureAuthenticated should not be done for Gerrit
852 uploads."""
853 if self.gerrit_skip_ensure_authenticated is None:
854 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000855 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000856 error_ok=True).strip() == 'true')
857 return self.gerrit_skip_ensure_authenticated
858
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000859 def GetGitEditor(self):
860 """Return the editor specified in the git config, or None if none is."""
861 if self.git_editor is None:
862 self.git_editor = self._GetConfig('core.editor', error_ok=True)
863 return self.git_editor or None
864
thestig@chromium.org44202a22014-03-11 19:22:18 +0000865 def GetLintRegex(self):
866 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
867 DEFAULT_LINT_REGEX)
868
869 def GetLintIgnoreRegex(self):
870 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
871 DEFAULT_LINT_IGNORE_REGEX)
872
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000873 def GetProject(self):
874 if not self.project:
875 self.project = self._GetRietveldConfig('project', error_ok=True)
876 return self.project
877
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000878 def _GetRietveldConfig(self, param, **kwargs):
879 return self._GetConfig('rietveld.' + param, **kwargs)
880
rmistry@google.com78948ed2015-07-08 23:09:57 +0000881 def _GetBranchConfig(self, branch_name, param, **kwargs):
882 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
883
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884 def _GetConfig(self, param, **kwargs):
885 self.LazyUpdateIfNeeded()
886 return RunGit(['config', param], **kwargs).strip()
887
888
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100889@contextlib.contextmanager
890def _get_gerrit_project_config_file(remote_url):
891 """Context manager to fetch and store Gerrit's project.config from
892 refs/meta/config branch and store it in temp file.
893
894 Provides a temporary filename or None if there was error.
895 """
896 error, _ = RunGitWithCode([
897 'fetch', remote_url,
898 '+refs/meta/config:refs/git_cl/meta/config'])
899 if error:
900 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700901 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100902 (remote_url, error))
903 yield None
904 return
905
906 error, project_config_data = RunGitWithCode(
907 ['show', 'refs/git_cl/meta/config:project.config'])
908 if error:
909 print('WARNING: project.config file not found')
910 yield None
911 return
912
913 with gclient_utils.temporary_directory() as tempdir:
914 project_config_file = os.path.join(tempdir, 'project.config')
915 gclient_utils.FileWrite(project_config_file, project_config_data)
916 yield project_config_file
917
918
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919def ShortBranchName(branch):
920 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000921 return branch.replace('refs/heads/', '', 1)
922
923
924def GetCurrentBranchRef():
925 """Returns branch ref (e.g., refs/heads/master) or None."""
926 return RunGit(['symbolic-ref', 'HEAD'],
927 stderr=subprocess2.VOID, error_ok=True).strip() or None
928
929
930def GetCurrentBranch():
931 """Returns current branch or None.
932
933 For refs/heads/* branches, returns just last part. For others, full ref.
934 """
935 branchref = GetCurrentBranchRef()
936 if branchref:
937 return ShortBranchName(branchref)
938 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939
940
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000941class _CQState(object):
942 """Enum for states of CL with respect to Commit Queue."""
943 NONE = 'none'
944 DRY_RUN = 'dry_run'
945 COMMIT = 'commit'
946
947 ALL_STATES = [NONE, DRY_RUN, COMMIT]
948
949
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000950class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200951 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000952 self.issue = issue
953 self.patchset = patchset
954 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200955 assert codereview in (None, 'rietveld', 'gerrit')
956 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000957
958 @property
959 def valid(self):
960 return self.issue is not None
961
962
Andrii Shyshkalovc9712392017-04-11 13:35:21 +0200963def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000964 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
965 fail_result = _ParsedIssueNumberArgument()
966
967 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -0700968 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000969 if not arg.startswith('http'):
970 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -0700971
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000972 url = gclient_utils.UpgradeToHttps(arg)
973 try:
974 parsed_url = urlparse.urlparse(url)
975 except ValueError:
976 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200977
Andrii Shyshkalovc9712392017-04-11 13:35:21 +0200978 if codereview is not None:
979 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
980 return parsed or fail_result
981
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200982 results = {}
983 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
984 parsed = cls.ParseIssueURL(parsed_url)
985 if parsed is not None:
986 results[name] = parsed
987
988 if not results:
989 return fail_result
990 if len(results) == 1:
991 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +0200992
993 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
994 # This is likely Gerrit.
995 return results['gerrit']
996 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200997 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000998
999
Aaron Gablea45ee112016-11-22 15:14:38 -08001000class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001001 def __init__(self, issue, url):
1002 self.issue = issue
1003 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001004 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001005
1006 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001007 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001008 self.issue, self.url)
1009
1010
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001011_CommentSummary = collections.namedtuple(
1012 '_CommentSummary', ['date', 'message', 'sender',
1013 # TODO(tandrii): these two aren't known in Gerrit.
1014 'approval', 'disapproval'])
1015
1016
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001017class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001018 """Changelist works with one changelist in local branch.
1019
1020 Supports two codereview backends: Rietveld or Gerrit, selected at object
1021 creation.
1022
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001023 Notes:
1024 * Not safe for concurrent multi-{thread,process} use.
1025 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001026 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001027 """
1028
1029 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1030 """Create a new ChangeList instance.
1031
1032 If issue is given, the codereview must be given too.
1033
1034 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1035 Otherwise, it's decided based on current configuration of the local branch,
1036 with default being 'rietveld' for backwards compatibility.
1037 See _load_codereview_impl for more details.
1038
1039 **kwargs will be passed directly to codereview implementation.
1040 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001042 global settings
1043 if not settings:
1044 # Happens when git_cl.py is used as a utility library.
1045 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001046
1047 if issue:
1048 assert codereview, 'codereview must be known, if issue is known'
1049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 self.branchref = branchref
1051 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001052 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053 self.branch = ShortBranchName(self.branchref)
1054 else:
1055 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001057 self.lookedup_issue = False
1058 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059 self.has_description = False
1060 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001061 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001062 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001063 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001064 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001065 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001066 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001067
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001068 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001069 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001070 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001071 assert self._codereview_impl
1072 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001073
1074 def _load_codereview_impl(self, codereview=None, **kwargs):
1075 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001076 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1077 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1078 self._codereview = codereview
1079 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001080 return
1081
1082 # Automatic selection based on issue number set for a current branch.
1083 # Rietveld takes precedence over Gerrit.
1084 assert not self.issue
1085 # Whether we find issue or not, we are doing the lookup.
1086 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001087 if self.GetBranch():
1088 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1089 issue = _git_get_branch_config_value(
1090 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1091 if issue:
1092 self._codereview = codereview
1093 self._codereview_impl = cls(self, **kwargs)
1094 self.issue = int(issue)
1095 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001096
1097 # No issue is set for this branch, so decide based on repo-wide settings.
1098 return self._load_codereview_impl(
1099 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1100 **kwargs)
1101
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001102 def IsGerrit(self):
1103 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001104
1105 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001106 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001107
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001108 The return value is a string suitable for passing to git cl with the --cc
1109 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001110 """
1111 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001112 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001113 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001114 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1115 return self.cc
1116
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001117 def GetCCListWithoutDefault(self):
1118 """Return the users cc'd on this CL excluding default ones."""
1119 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001120 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001121 return self.cc
1122
Daniel Cheng7227d212017-11-17 08:12:37 -08001123 def ExtendCC(self, more_cc):
1124 """Extends the list of users to cc on this CL based on the changed files."""
1125 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126
1127 def GetBranch(self):
1128 """Returns the short branch name, e.g. 'master'."""
1129 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001130 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001131 if not branchref:
1132 return None
1133 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 self.branch = ShortBranchName(self.branchref)
1135 return self.branch
1136
1137 def GetBranchRef(self):
1138 """Returns the full branch name, e.g. 'refs/heads/master'."""
1139 self.GetBranch() # Poke the lazy loader.
1140 return self.branchref
1141
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001142 def ClearBranch(self):
1143 """Clears cached branch data of this object."""
1144 self.branch = self.branchref = None
1145
tandrii5d48c322016-08-18 16:19:37 -07001146 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1147 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1148 kwargs['branch'] = self.GetBranch()
1149 return _git_get_branch_config_value(key, default, **kwargs)
1150
1151 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1152 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1153 assert self.GetBranch(), (
1154 'this CL must have an associated branch to %sset %s%s' %
1155 ('un' if value is None else '',
1156 key,
1157 '' if value is None else ' to %r' % value))
1158 kwargs['branch'] = self.GetBranch()
1159 return _git_set_branch_config_value(key, value, **kwargs)
1160
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001161 @staticmethod
1162 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001163 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 e.g. 'origin', 'refs/heads/master'
1165 """
1166 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001167 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1168
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001170 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001172 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1173 error_ok=True).strip()
1174 if upstream_branch:
1175 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001177 # Else, try to guess the origin remote.
1178 remote_branches = RunGit(['branch', '-r']).split()
1179 if 'origin/master' in remote_branches:
1180 # Fall back on origin/master if it exits.
1181 remote = 'origin'
1182 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001184 DieWithError(
1185 'Unable to determine default branch to diff against.\n'
1186 'Either pass complete "git diff"-style arguments, like\n'
1187 ' git cl upload origin/master\n'
1188 'or verify this branch is set up to track another \n'
1189 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190
1191 return remote, upstream_branch
1192
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001193 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001194 upstream_branch = self.GetUpstreamBranch()
1195 if not BranchExists(upstream_branch):
1196 DieWithError('The upstream for the current branch (%s) does not exist '
1197 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001198 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001199 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001200
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 def GetUpstreamBranch(self):
1202 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001203 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001205 upstream_branch = upstream_branch.replace('refs/heads/',
1206 'refs/remotes/%s/' % remote)
1207 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1208 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 self.upstream_branch = upstream_branch
1210 return self.upstream_branch
1211
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001212 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001213 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001214 remote, branch = None, self.GetBranch()
1215 seen_branches = set()
1216 while branch not in seen_branches:
1217 seen_branches.add(branch)
1218 remote, branch = self.FetchUpstreamTuple(branch)
1219 branch = ShortBranchName(branch)
1220 if remote != '.' or branch.startswith('refs/remotes'):
1221 break
1222 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001223 remotes = RunGit(['remote'], error_ok=True).split()
1224 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001225 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001226 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001227 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001228 logging.warn('Could not determine which remote this change is '
1229 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001230 else:
1231 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001232 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001233 branch = 'HEAD'
1234 if branch.startswith('refs/remotes'):
1235 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001236 elif branch.startswith('refs/branch-heads/'):
1237 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001238 else:
1239 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001240 return self._remote
1241
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001242 def GitSanityChecks(self, upstream_git_obj):
1243 """Checks git repo status and ensures diff is from local commits."""
1244
sbc@chromium.org79706062015-01-14 21:18:12 +00001245 if upstream_git_obj is None:
1246 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001247 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001248 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001249 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001250 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001251 return False
1252
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001253 # Verify the commit we're diffing against is in our current branch.
1254 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1255 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1256 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001257 print('ERROR: %s is not in the current branch. You may need to rebase '
1258 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001259 return False
1260
1261 # List the commits inside the diff, and verify they are all local.
1262 commits_in_diff = RunGit(
1263 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1264 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1265 remote_branch = remote_branch.strip()
1266 if code != 0:
1267 _, remote_branch = self.GetRemoteBranch()
1268
1269 commits_in_remote = RunGit(
1270 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1271
1272 common_commits = set(commits_in_diff) & set(commits_in_remote)
1273 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001274 print('ERROR: Your diff contains %d commits already in %s.\n'
1275 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1276 'the diff. If you are using a custom git flow, you can override'
1277 ' the reference used for this check with "git config '
1278 'gitcl.remotebranch <git-ref>".' % (
1279 len(common_commits), remote_branch, upstream_git_obj),
1280 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001281 return False
1282 return True
1283
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001284 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001285 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001286
1287 Returns None if it is not set.
1288 """
tandrii5d48c322016-08-18 16:19:37 -07001289 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 def GetRemoteUrl(self):
1292 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1293
1294 Returns None if there is no remote.
1295 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001296 is_cached, value = self._cached_remote_url
1297 if is_cached:
1298 return value
1299
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001301 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1302
1303 # If URL is pointing to a local directory, it is probably a git cache.
1304 if os.path.isdir(url):
1305 url = RunGit(['config', 'remote.%s.url' % remote],
1306 error_ok=True,
1307 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001308 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001309 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001311 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001312 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001313 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001314 self.issue = self._GitGetBranchConfigValue(
1315 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001316 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317 return self.issue
1318
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319 def GetIssueURL(self):
1320 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001321 issue = self.GetIssue()
1322 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001323 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001324 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001326 def GetDescription(self, pretty=False, force=False):
1327 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001329 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 self.has_description = True
1331 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001332 # Set width to 72 columns + 2 space indent.
1333 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001335 lines = self.description.splitlines()
1336 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 return self.description
1338
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001339 def GetDescriptionFooters(self):
1340 """Returns (non_footer_lines, footers) for the commit message.
1341
1342 Returns:
1343 non_footer_lines (list(str)) - Simple list of description lines without
1344 any footer. The lines do not contain newlines, nor does the list contain
1345 the empty line between the message and the footers.
1346 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1347 [("Change-Id", "Ideadbeef...."), ...]
1348 """
1349 raw_description = self.GetDescription()
1350 msg_lines, _, footers = git_footers.split_footers(raw_description)
1351 if footers:
1352 msg_lines = msg_lines[:len(msg_lines)-1]
1353 return msg_lines, footers
1354
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001356 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001357 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001358 self.patchset = self._GitGetBranchConfigValue(
1359 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001360 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361 return self.patchset
1362
1363 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001364 """Set this branch's patchset. If patchset=0, clears the patchset."""
1365 assert self.GetBranch()
1366 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001367 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001368 else:
1369 self.patchset = int(patchset)
1370 self._GitSetBranchConfigValue(
1371 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001373 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001374 """Set this branch's issue. If issue isn't given, clears the issue."""
1375 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001376 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001377 issue = int(issue)
1378 self._GitSetBranchConfigValue(
1379 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001380 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001381 codereview_server = self._codereview_impl.GetCodereviewServer()
1382 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001383 self._GitSetBranchConfigValue(
1384 self._codereview_impl.CodereviewServerConfigKey(),
1385 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001386 else:
tandrii5d48c322016-08-18 16:19:37 -07001387 # Reset all of these just to be clean.
1388 reset_suffixes = [
1389 'last-upload-hash',
1390 self._codereview_impl.IssueConfigKey(),
1391 self._codereview_impl.PatchsetConfigKey(),
1392 self._codereview_impl.CodereviewServerConfigKey(),
1393 ] + self._PostUnsetIssueProperties()
1394 for prop in reset_suffixes:
1395 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001396 msg = RunGit(['log', '-1', '--format=%B']).strip()
1397 if msg and git_footers.get_footer_change_id(msg):
1398 print('WARNING: The change patched into this branch has a Change-Id. '
1399 'Removing it.')
1400 RunGit(['commit', '--amend', '-m',
1401 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001402 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001403 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404
dnjba1b0f32016-09-02 12:37:42 -07001405 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001406 if not self.GitSanityChecks(upstream_branch):
1407 DieWithError('\nGit sanity check failure')
1408
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001409 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001410 if not root:
1411 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001412 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001413
1414 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001415 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001416 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001417 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001418 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001419 except subprocess2.CalledProcessError:
1420 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001421 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001422 'This branch probably doesn\'t exist anymore. To reset the\n'
1423 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001424 ' git branch --set-upstream-to origin/master %s\n'
1425 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001426 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001427
maruel@chromium.org52424302012-08-29 15:14:30 +00001428 issue = self.GetIssue()
1429 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001430 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001431 description = self.GetDescription()
1432 else:
1433 # If the change was never uploaded, use the log messages of all commits
1434 # up to the branch point, as git cl upload will prefill the description
1435 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001436 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1437 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001438
1439 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001440 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001441 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001442 name,
1443 description,
1444 absroot,
1445 files,
1446 issue,
1447 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001448 author,
1449 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001450
dsansomee2d6fd92016-09-08 00:10:47 -07001451 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001452 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001453 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001454 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001455
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001456 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1457 """Sets the description for this CL remotely.
1458
1459 You can get description_lines and footers with GetDescriptionFooters.
1460
1461 Args:
1462 description_lines (list(str)) - List of CL description lines without
1463 newline characters.
1464 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1465 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1466 `List-Of-Tokens`). It will be case-normalized so that each token is
1467 title-cased.
1468 """
1469 new_description = '\n'.join(description_lines)
1470 if footers:
1471 new_description += '\n'
1472 for k, v in footers:
1473 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1474 if not git_footers.FOOTER_PATTERN.match(foot):
1475 raise ValueError('Invalid footer %r' % foot)
1476 new_description += foot + '\n'
1477 self.UpdateDescription(new_description, force)
1478
Edward Lesmes8e282792018-04-03 18:50:29 -04001479 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001480 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1481 try:
1482 return presubmit_support.DoPresubmitChecks(change, committing,
1483 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1484 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001485 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1486 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001487 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001488 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001489
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001490 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1491 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001492 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1493 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001494 else:
1495 # Assume url.
1496 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1497 urlparse.urlparse(issue_arg))
1498 if not parsed_issue_arg or not parsed_issue_arg.valid:
1499 DieWithError('Failed to parse issue argument "%s". '
1500 'Must be an issue number or a valid URL.' % issue_arg)
1501 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001502 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001503
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001504 def CMDUpload(self, options, git_diff_args, orig_args):
1505 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001506 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001507 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001508 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001509 else:
1510 if self.GetBranch() is None:
1511 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1512
1513 # Default to diffing against common ancestor of upstream branch
1514 base_branch = self.GetCommonAncestorWithUpstream()
1515 git_diff_args = [base_branch, 'HEAD']
1516
Aaron Gablec4c40d12017-05-22 11:49:53 -07001517 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1518 if not self.IsGerrit() and not self.GetIssue():
1519 print('=====================================')
1520 print('NOTICE: Rietveld is being deprecated. '
1521 'You can upload changes to Gerrit with')
1522 print(' git cl upload --gerrit')
1523 print('or set Gerrit to be your default code review tool with')
1524 print(' git config gerrit.host true')
1525 print('=====================================')
1526
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001527 # Fast best-effort checks to abort before running potentially
1528 # expensive hooks if uploading is likely to fail anyway. Passing these
1529 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001530 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001531 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001532
1533 # Apply watchlists on upload.
1534 change = self.GetChange(base_branch, None)
1535 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1536 files = [f.LocalPath() for f in change.AffectedFiles()]
1537 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001538 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001539
1540 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001541 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001542 # Set the reviewer list now so that presubmit checks can access it.
1543 change_description = ChangeDescription(change.FullDescriptionText())
1544 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001545 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001546 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001547 change)
1548 change.SetDescriptionText(change_description.description)
1549 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001550 may_prompt=not options.force,
1551 verbose=options.verbose,
1552 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001553 if not hook_results.should_continue():
1554 return 1
1555 if not options.reviewers and hook_results.reviewers:
1556 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001557 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001558
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001559 # TODO(tandrii): Checking local patchset against remote patchset is only
1560 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1561 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001562 latest_patchset = self.GetMostRecentPatchset()
1563 local_patchset = self.GetPatchset()
1564 if (latest_patchset and local_patchset and
1565 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001566 print('The last upload made from this repository was patchset #%d but '
1567 'the most recent patchset on the server is #%d.'
1568 % (local_patchset, latest_patchset))
1569 print('Uploading will still work, but if you\'ve uploaded to this '
1570 'issue from another machine or branch the patch you\'re '
1571 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001572 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001573
Aaron Gable13101a62018-02-09 13:20:41 -08001574 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001575 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001576 if not ret:
Ravi Mistry31e7d562018-04-02 12:53:57 -04001577 if self.IsGerrit():
1578 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
1579 options.cq_dry_run);
1580 else:
1581 if options.use_commit_queue:
1582 self.SetCQState(_CQState.COMMIT)
1583 elif options.cq_dry_run:
1584 self.SetCQState(_CQState.DRY_RUN)
tandrii4d0545a2016-07-06 03:56:49 -07001585
tandrii5d48c322016-08-18 16:19:37 -07001586 _git_set_branch_config_value('last-upload-hash',
1587 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001588 # Run post upload hooks, if specified.
1589 if settings.GetRunPostUploadHook():
1590 presubmit_support.DoPostUploadExecuter(
1591 change,
1592 self,
1593 settings.GetRoot(),
1594 options.verbose,
1595 sys.stdout)
1596
1597 # Upload all dependencies if specified.
1598 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001599 print()
1600 print('--dependencies has been specified.')
1601 print('All dependent local branches will be re-uploaded.')
1602 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001603 # Remove the dependencies flag from args so that we do not end up in a
1604 # loop.
1605 orig_args.remove('--dependencies')
1606 ret = upload_branch_deps(self, orig_args)
1607 return ret
1608
Ravi Mistry31e7d562018-04-02 12:53:57 -04001609 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1610 """Sets labels on the change based on the provided flags.
1611
1612 Sets labels if issue is already uploaded and known, else returns without
1613 doing anything.
1614
1615 Args:
1616 enable_auto_submit: Sets Auto-Submit+1 on the change.
1617 use_commit_queue: Sets Commit-Queue+2 on the change.
1618 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1619 both use_commit_queue and cq_dry_run are true.
1620 """
1621 if not self.GetIssue():
1622 return
1623 try:
1624 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1625 cq_dry_run)
1626 return 0
1627 except KeyboardInterrupt:
1628 raise
1629 except:
1630 labels = []
1631 if enable_auto_submit:
1632 labels.append('Auto-Submit')
1633 if use_commit_queue or cq_dry_run:
1634 labels.append('Commit-Queue')
1635 print('WARNING: Failed to set label(s) on your change: %s\n'
1636 'Either:\n'
1637 ' * Your project does not have the above label(s),\n'
1638 ' * You don\'t have permission to set the above label(s),\n'
1639 ' * There\'s a bug in this code (see stack trace below).\n' %
1640 (', '.join(labels)))
1641 # Still raise exception so that stack trace is printed.
1642 raise
1643
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001644 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001645 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001646
1647 Issue must have been already uploaded and known.
1648 """
1649 assert new_state in _CQState.ALL_STATES
1650 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001651 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001652 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001653 return 0
1654 except KeyboardInterrupt:
1655 raise
1656 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001657 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001658 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001659 ' * Your project has no CQ,\n'
1660 ' * You don\'t have permission to change the CQ state,\n'
1661 ' * There\'s a bug in this code (see stack trace below).\n'
1662 'Consider specifying which bots to trigger manually or asking your '
1663 'project owners for permissions or contacting Chrome Infra at:\n'
1664 'https://www.chromium.org/infra\n\n' %
1665 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001666 # Still raise exception so that stack trace is printed.
1667 raise
1668
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001669 # Forward methods to codereview specific implementation.
1670
Aaron Gable636b13f2017-07-14 10:42:48 -07001671 def AddComment(self, message, publish=None):
1672 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001673
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001674 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001675 """Returns list of _CommentSummary for each comment.
1676
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001677 args:
1678 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001679 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001680 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001681
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001682 def CloseIssue(self):
1683 return self._codereview_impl.CloseIssue()
1684
1685 def GetStatus(self):
1686 return self._codereview_impl.GetStatus()
1687
1688 def GetCodereviewServer(self):
1689 return self._codereview_impl.GetCodereviewServer()
1690
tandriide281ae2016-10-12 06:02:30 -07001691 def GetIssueOwner(self):
1692 """Get owner from codereview, which may differ from this checkout."""
1693 return self._codereview_impl.GetIssueOwner()
1694
Edward Lemur707d70b2018-02-07 00:50:14 +01001695 def GetReviewers(self):
1696 return self._codereview_impl.GetReviewers()
1697
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001698 def GetMostRecentPatchset(self):
1699 return self._codereview_impl.GetMostRecentPatchset()
1700
tandriide281ae2016-10-12 06:02:30 -07001701 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001702 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001703 return self._codereview_impl.CannotTriggerTryJobReason()
1704
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001705 def GetTryJobProperties(self, patchset=None):
1706 """Returns dictionary of properties to launch try job."""
1707 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001708
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001709 def __getattr__(self, attr):
1710 # This is because lots of untested code accesses Rietveld-specific stuff
1711 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001712 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001713 # Note that child method defines __getattr__ as well, and forwards it here,
1714 # because _RietveldChangelistImpl is not cleaned up yet, and given
1715 # deprecation of Rietveld, it should probably be just removed.
1716 # Until that time, avoid infinite recursion by bypassing __getattr__
1717 # of implementation class.
1718 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001719
1720
1721class _ChangelistCodereviewBase(object):
1722 """Abstract base class encapsulating codereview specifics of a changelist."""
1723 def __init__(self, changelist):
1724 self._changelist = changelist # instance of Changelist
1725
1726 def __getattr__(self, attr):
1727 # Forward methods to changelist.
1728 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1729 # _RietveldChangelistImpl to avoid this hack?
1730 return getattr(self._changelist, attr)
1731
1732 def GetStatus(self):
1733 """Apply a rough heuristic to give a simple summary of an issue's review
1734 or CQ status, assuming adherence to a common workflow.
1735
1736 Returns None if no issue for this branch, or specific string keywords.
1737 """
1738 raise NotImplementedError()
1739
1740 def GetCodereviewServer(self):
1741 """Returns server URL without end slash, like "https://codereview.com"."""
1742 raise NotImplementedError()
1743
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001744 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001745 """Fetches and returns description from the codereview server."""
1746 raise NotImplementedError()
1747
tandrii5d48c322016-08-18 16:19:37 -07001748 @classmethod
1749 def IssueConfigKey(cls):
1750 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001751 raise NotImplementedError()
1752
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001753 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001754 def PatchsetConfigKey(cls):
1755 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001756 raise NotImplementedError()
1757
tandrii5d48c322016-08-18 16:19:37 -07001758 @classmethod
1759 def CodereviewServerConfigKey(cls):
1760 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001761 raise NotImplementedError()
1762
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001763 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001764 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001765 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001766
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001767 def GetGerritObjForPresubmit(self):
1768 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1769 return None
1770
dsansomee2d6fd92016-09-08 00:10:47 -07001771 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001772 """Update the description on codereview site."""
1773 raise NotImplementedError()
1774
Aaron Gable636b13f2017-07-14 10:42:48 -07001775 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001776 """Posts a comment to the codereview site."""
1777 raise NotImplementedError()
1778
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001779 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001780 raise NotImplementedError()
1781
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001782 def CloseIssue(self):
1783 """Closes the issue."""
1784 raise NotImplementedError()
1785
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001786 def GetMostRecentPatchset(self):
1787 """Returns the most recent patchset number from the codereview site."""
1788 raise NotImplementedError()
1789
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001790 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001791 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001792 """Fetches and applies the issue.
1793
1794 Arguments:
1795 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1796 reject: if True, reject the failed patch instead of switching to 3-way
1797 merge. Rietveld only.
1798 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1799 only.
1800 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001801 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001802 """
1803 raise NotImplementedError()
1804
1805 @staticmethod
1806 def ParseIssueURL(parsed_url):
1807 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1808 failed."""
1809 raise NotImplementedError()
1810
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001811 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001812 """Best effort check that user is authenticated with codereview server.
1813
1814 Arguments:
1815 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001816 refresh: whether to attempt to refresh credentials. Ignored if not
1817 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001818 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001819 raise NotImplementedError()
1820
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001821 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001822 """Best effort check that uploading isn't supposed to fail for predictable
1823 reasons.
1824
1825 This method should raise informative exception if uploading shouldn't
1826 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001827
1828 Arguments:
1829 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001830 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001831 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001832
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001833 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001834 """Uploads a change to codereview."""
1835 raise NotImplementedError()
1836
Ravi Mistry31e7d562018-04-02 12:53:57 -04001837 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1838 """Sets labels on the change based on the provided flags.
1839
1840 Issue must have been already uploaded and known.
1841 """
1842 raise NotImplementedError()
1843
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001844 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001845 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001846
1847 Issue must have been already uploaded and known.
1848 """
1849 raise NotImplementedError()
1850
tandriie113dfd2016-10-11 10:20:12 -07001851 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001852 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001853 raise NotImplementedError()
1854
tandriide281ae2016-10-12 06:02:30 -07001855 def GetIssueOwner(self):
1856 raise NotImplementedError()
1857
Edward Lemur707d70b2018-02-07 00:50:14 +01001858 def GetReviewers(self):
1859 raise NotImplementedError()
1860
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001861 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001862 raise NotImplementedError()
1863
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001864
1865class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001866
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001867 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001868 super(_RietveldChangelistImpl, self).__init__(changelist)
1869 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001870 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001871 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001872
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001873 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001874 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001875 self._props = None
1876 self._rpc_server = None
1877
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001878 def GetCodereviewServer(self):
1879 if not self._rietveld_server:
1880 # If we're on a branch then get the server potentially associated
1881 # with that branch.
1882 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001883 self._rietveld_server = gclient_utils.UpgradeToHttps(
1884 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001885 if not self._rietveld_server:
1886 self._rietveld_server = settings.GetDefaultServerUrl()
1887 return self._rietveld_server
1888
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001889 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001890 """Best effort check that user is authenticated with Rietveld server."""
1891 if self._auth_config.use_oauth2:
1892 authenticator = auth.get_authenticator_for_host(
1893 self.GetCodereviewServer(), self._auth_config)
1894 if not authenticator.has_cached_credentials():
1895 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001896 if refresh:
1897 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001898
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001899 def EnsureCanUploadPatchset(self, force):
1900 # No checks for Rietveld because we are deprecating Rietveld.
1901 pass
1902
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001903 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001904 issue = self.GetIssue()
1905 assert issue
1906 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001907 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001908 except urllib2.HTTPError as e:
1909 if e.code == 404:
1910 DieWithError(
1911 ('\nWhile fetching the description for issue %d, received a '
1912 '404 (not found)\n'
1913 'error. It is likely that you deleted this '
1914 'issue on the server. If this is the\n'
1915 'case, please run\n\n'
1916 ' git cl issue 0\n\n'
1917 'to clear the association with the deleted issue. Then run '
1918 'this command again.') % issue)
1919 else:
1920 DieWithError(
1921 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1922 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001923 print('Warning: Failed to retrieve CL description due to network '
1924 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001925 return ''
1926
1927 def GetMostRecentPatchset(self):
1928 return self.GetIssueProperties()['patchsets'][-1]
1929
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001930 def GetIssueProperties(self):
1931 if self._props is None:
1932 issue = self.GetIssue()
1933 if not issue:
1934 self._props = {}
1935 else:
1936 self._props = self.RpcServer().get_issue_properties(issue, True)
1937 return self._props
1938
tandriie113dfd2016-10-11 10:20:12 -07001939 def CannotTriggerTryJobReason(self):
1940 props = self.GetIssueProperties()
1941 if not props:
1942 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1943 if props.get('closed'):
1944 return 'CL %s is closed' % self.GetIssue()
1945 if props.get('private'):
1946 return 'CL %s is private' % self.GetIssue()
1947 return None
1948
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001949 def GetTryJobProperties(self, patchset=None):
1950 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07001951 project = (self.GetIssueProperties() or {}).get('project')
1952 return {
1953 'issue': self.GetIssue(),
1954 'patch_project': project,
1955 'patch_storage': 'rietveld',
1956 'patchset': patchset or self.GetPatchset(),
1957 'rietveld': self.GetCodereviewServer(),
1958 }
1959
tandriide281ae2016-10-12 06:02:30 -07001960 def GetIssueOwner(self):
1961 return (self.GetIssueProperties() or {}).get('owner_email')
1962
Edward Lemur707d70b2018-02-07 00:50:14 +01001963 def GetReviewers(self):
1964 return (self.GetIssueProperties() or {}).get('reviewers')
1965
Aaron Gable636b13f2017-07-14 10:42:48 -07001966 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001967 return self.RpcServer().add_comment(self.GetIssue(), message)
1968
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001969 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001970 summary = []
1971 for message in self.GetIssueProperties().get('messages', []):
1972 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
1973 summary.append(_CommentSummary(
1974 date=date,
1975 disapproval=bool(message['disapproval']),
1976 approval=bool(message['approval']),
1977 sender=message['sender'],
1978 message=message['text'],
1979 ))
1980 return summary
1981
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001982 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001983 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001984 or CQ status, assuming adherence to a common workflow.
1985
1986 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07001987 * 'error' - error from review tool (including deleted issues)
1988 * 'unsent' - not sent for review
1989 * 'waiting' - waiting for review
1990 * 'reply' - waiting for owner to reply to review
1991 * 'not lgtm' - Code-Review label has been set negatively
1992 * 'lgtm' - LGTM from at least one approved reviewer
1993 * 'commit' - in the commit queue
1994 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001995 """
1996 if not self.GetIssue():
1997 return None
1998
1999 try:
2000 props = self.GetIssueProperties()
2001 except urllib2.HTTPError:
2002 return 'error'
2003
2004 if props.get('closed'):
2005 # Issue is closed.
2006 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002007 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002008 # Issue is in the commit queue.
2009 return 'commit'
2010
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002011 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002012 if not messages:
2013 # No message was sent.
2014 return 'unsent'
2015
2016 if get_approving_reviewers(props):
2017 return 'lgtm'
2018 elif get_approving_reviewers(props, disapproval=True):
2019 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002020
tandrii9d2c7a32016-06-22 03:42:45 -07002021 # Skip CQ messages that don't require owner's action.
2022 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2023 if 'Dry run:' in messages[-1]['text']:
2024 messages.pop()
2025 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2026 # This message always follows prior messages from CQ,
2027 # so skip this too.
2028 messages.pop()
2029 else:
2030 # This is probably a CQ messages warranting user attention.
2031 break
2032
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002033 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002034 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002035 return 'reply'
2036 return 'waiting'
2037
dsansomee2d6fd92016-09-08 00:10:47 -07002038 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002039 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002040
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002041 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002042 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002044 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002045 return self.SetFlags({flag: value})
2046
2047 def SetFlags(self, flags):
2048 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002049 """
phajdan.jr68598232016-08-10 03:28:28 -07002050 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002051 try:
tandrii4b233bd2016-07-06 03:50:29 -07002052 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002053 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002054 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002055 if e.code == 404:
2056 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2057 if e.code == 403:
2058 DieWithError(
2059 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002060 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002061 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002062
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002063 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002064 """Returns an upload.RpcServer() to access this review's rietveld instance.
2065 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002066 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002067 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002068 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002069 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002070 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002071
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002072 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002073 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002074 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002075
tandrii5d48c322016-08-18 16:19:37 -07002076 @classmethod
2077 def PatchsetConfigKey(cls):
2078 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002079
tandrii5d48c322016-08-18 16:19:37 -07002080 @classmethod
2081 def CodereviewServerConfigKey(cls):
2082 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002083
Ravi Mistry31e7d562018-04-02 12:53:57 -04002084 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2085 raise NotImplementedError()
2086
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002087 def SetCQState(self, new_state):
2088 props = self.GetIssueProperties()
2089 if props.get('private'):
2090 DieWithError('Cannot set-commit on private issue')
2091
2092 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002093 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002094 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002095 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002096 else:
tandrii4b233bd2016-07-06 03:50:29 -07002097 assert new_state == _CQState.DRY_RUN
2098 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002099
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002100 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002101 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002102 # PatchIssue should never be called with a dirty tree. It is up to the
2103 # caller to check this, but just in case we assert here since the
2104 # consequences of the caller not checking this could be dire.
2105 assert(not git_common.is_dirty_git_tree('apply'))
2106 assert(parsed_issue_arg.valid)
2107 self._changelist.issue = parsed_issue_arg.issue
2108 if parsed_issue_arg.hostname:
2109 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2110
skobes6468b902016-10-24 08:45:10 -07002111 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2112 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2113 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002114 try:
skobes6468b902016-10-24 08:45:10 -07002115 scm_obj.apply_patch(patchset_object)
2116 except Exception as e:
2117 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002118 return 1
2119
2120 # If we had an issue, commit the current state and register the issue.
2121 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002122 self.SetIssue(self.GetIssue())
2123 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002124 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2125 'patch from issue %(i)s at patchset '
2126 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2127 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002128 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002129 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002130 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002131 return 0
2132
2133 @staticmethod
2134 def ParseIssueURL(parsed_url):
2135 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2136 return None
wychen3c1c1722016-08-04 11:46:36 -07002137 # Rietveld patch: https://domain/<number>/#ps<patchset>
2138 match = re.match(r'/(\d+)/$', parsed_url.path)
2139 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2140 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002141 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002142 issue=int(match.group(1)),
2143 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002144 hostname=parsed_url.netloc,
2145 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002146 # Typical url: https://domain/<issue_number>[/[other]]
2147 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2148 if match:
skobes6468b902016-10-24 08:45:10 -07002149 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002150 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002151 hostname=parsed_url.netloc,
2152 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002153 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2154 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2155 if match:
skobes6468b902016-10-24 08:45:10 -07002156 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002157 issue=int(match.group(1)),
2158 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002159 hostname=parsed_url.netloc,
2160 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002161 return None
2162
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002163 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002164 """Upload the patch to Rietveld."""
2165 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2166 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002167 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2168 if options.emulate_svn_auto_props:
2169 upload_args.append('--emulate_svn_auto_props')
2170
2171 change_desc = None
2172
2173 if options.email is not None:
2174 upload_args.extend(['--email', options.email])
2175
2176 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002177 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002178 upload_args.extend(['--title', options.title])
2179 if options.message:
2180 upload_args.extend(['--message', options.message])
2181 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002182 print('This branch is associated with issue %s. '
2183 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002184 else:
nodirca166002016-06-27 10:59:51 -07002185 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002186 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002187 if options.message:
2188 message = options.message
2189 else:
2190 message = CreateDescriptionFromLog(args)
2191 if options.title:
2192 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002193 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002194 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002195 change_desc.update_reviewers(options.reviewers, options.tbrs,
2196 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002197 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002198 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002199
2200 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002201 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002202 return 1
2203
2204 upload_args.extend(['--message', change_desc.description])
2205 if change_desc.get_reviewers():
2206 upload_args.append('--reviewers=%s' % ','.join(
2207 change_desc.get_reviewers()))
2208 if options.send_mail:
2209 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002210 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211 upload_args.append('--send_mail')
2212
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00002213 # We only skip auto-CC-ing addresses from rietveld.cc when --private or
2214 # --no-autocc is explicitly specified on the command line. Should private
2215 # CL be created due to rietveld.private value, we assume that rietveld.cc
2216 # only contains addresses where private CLs are allowed to be sent.
2217 if options.private or options.no_autocc:
2218 logging.warn('rietveld.cc is ignored since private/no-autocc flag is '
2219 'specified. You need to review and add them manually if '
2220 'necessary.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002221 cc = self.GetCCListWithoutDefault()
2222 else:
2223 cc = self.GetCCList()
2224 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002225 if change_desc.get_cced():
2226 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002227 if cc:
2228 upload_args.extend(['--cc', cc])
2229
2230 if options.private or settings.GetDefaultPrivateFlag() == "True":
2231 upload_args.append('--private')
2232
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002233 # Include the upstream repo's URL in the change -- this is useful for
2234 # projects that have their source spread across multiple repos.
2235 remote_url = self.GetGitBaseUrlFromConfig()
2236 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002237 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2238 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2239 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002240 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002241 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002242 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002243 if target_ref:
2244 upload_args.extend(['--target_ref', target_ref])
2245
2246 # Look for dependent patchsets. See crbug.com/480453 for more details.
2247 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2248 upstream_branch = ShortBranchName(upstream_branch)
2249 if remote is '.':
2250 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002251 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002252 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002253 print()
2254 print('Skipping dependency patchset upload because git config '
2255 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2256 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002257 else:
2258 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002259 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002260 auth_config=auth_config)
2261 branch_cl_issue_url = branch_cl.GetIssueURL()
2262 branch_cl_issue = branch_cl.GetIssue()
2263 branch_cl_patchset = branch_cl.GetPatchset()
2264 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2265 upload_args.extend(
2266 ['--depends_on_patchset', '%s:%s' % (
2267 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002268 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002269 '\n'
2270 'The current branch (%s) is tracking a local branch (%s) with '
2271 'an associated CL.\n'
2272 'Adding %s/#ps%s as a dependency patchset.\n'
2273 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2274 branch_cl_patchset))
2275
2276 project = settings.GetProject()
2277 if project:
2278 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002279 else:
2280 print()
2281 print('WARNING: Uploading without a project specified. Please ensure '
2282 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2283 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002284
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002285 try:
2286 upload_args = ['upload'] + upload_args + args
2287 logging.info('upload.RealMain(%s)', upload_args)
2288 issue, patchset = upload.RealMain(upload_args)
2289 issue = int(issue)
2290 patchset = int(patchset)
2291 except KeyboardInterrupt:
2292 sys.exit(1)
2293 except:
2294 # If we got an exception after the user typed a description for their
2295 # change, back up the description before re-raising.
2296 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002297 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002298 raise
2299
2300 if not self.GetIssue():
2301 self.SetIssue(issue)
2302 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002303 return 0
2304
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002305
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002306class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002307 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002308 # auth_config is Rietveld thing, kept here to preserve interface only.
2309 super(_GerritChangelistImpl, self).__init__(changelist)
2310 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002311 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002312 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002313 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002314 # Map from change number (issue) to its detail cache.
2315 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002316
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002317 if codereview_host is not None:
2318 assert not codereview_host.startswith('https://'), codereview_host
2319 self._gerrit_host = codereview_host
2320 self._gerrit_server = 'https://%s' % codereview_host
2321
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002322 def _GetGerritHost(self):
2323 # Lazy load of configs.
2324 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002325 if self._gerrit_host and '.' not in self._gerrit_host:
2326 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2327 # This happens for internal stuff http://crbug.com/614312.
2328 parsed = urlparse.urlparse(self.GetRemoteUrl())
2329 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002330 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002331 ' Your current remote is: %s' % self.GetRemoteUrl())
2332 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2333 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002334 return self._gerrit_host
2335
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002336 def _GetGitHost(self):
2337 """Returns git host to be used when uploading change to Gerrit."""
2338 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2339
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002340 def GetCodereviewServer(self):
2341 if not self._gerrit_server:
2342 # If we're on a branch then get the server potentially associated
2343 # with that branch.
2344 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002345 self._gerrit_server = self._GitGetBranchConfigValue(
2346 self.CodereviewServerConfigKey())
2347 if self._gerrit_server:
2348 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002349 if not self._gerrit_server:
2350 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2351 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002352 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002353 parts[0] = parts[0] + '-review'
2354 self._gerrit_host = '.'.join(parts)
2355 self._gerrit_server = 'https://%s' % self._gerrit_host
2356 return self._gerrit_server
2357
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002358 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002359 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002360 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002361 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002362 logging.warn('can\'t detect Gerrit project.')
2363 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002364 project = urlparse.urlparse(remote_url).path.strip('/')
2365 if project.endswith('.git'):
2366 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00002367 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
2368 # 'a/' prefix, because 'a/' prefix is used to force authentication in
2369 # gitiles/git-over-https protocol. E.g.,
2370 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
2371 # as
2372 # https://chromium.googlesource.com/v8/v8
2373 if project.startswith('a/'):
2374 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002375 return project
2376
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002377 def _GerritChangeIdentifier(self):
2378 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2379
2380 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002381 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002382 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002383 project = self._GetGerritProject()
2384 if project:
2385 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2386 # Fall back on still unique, but less efficient change number.
2387 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002388
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002389 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002390 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002391 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002392
tandrii5d48c322016-08-18 16:19:37 -07002393 @classmethod
2394 def PatchsetConfigKey(cls):
2395 return 'gerritpatchset'
2396
2397 @classmethod
2398 def CodereviewServerConfigKey(cls):
2399 return 'gerritserver'
2400
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002401 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002402 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002403 if settings.GetGerritSkipEnsureAuthenticated():
2404 # For projects with unusual authentication schemes.
2405 # See http://crbug.com/603378.
2406 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002407 # Lazy-loader to identify Gerrit and Git hosts.
2408 if gerrit_util.GceAuthenticator.is_gce():
2409 return
2410 self.GetCodereviewServer()
2411 git_host = self._GetGitHost()
2412 assert self._gerrit_server and self._gerrit_host
2413 cookie_auth = gerrit_util.CookiesAuthenticator()
2414
2415 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2416 git_auth = cookie_auth.get_auth_header(git_host)
2417 if gerrit_auth and git_auth:
2418 if gerrit_auth == git_auth:
2419 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002420 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002421 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002422 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002423 ' %s\n'
2424 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002425 ' Consider running the following command:\n'
2426 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002427 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002428 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002429 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002430 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002431 cookie_auth.get_new_password_message(git_host)))
2432 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002433 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002434 return
2435 else:
2436 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002437 ([] if gerrit_auth else [self._gerrit_host]) +
2438 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002439 DieWithError('Credentials for the following hosts are required:\n'
2440 ' %s\n'
2441 'These are read from %s (or legacy %s)\n'
2442 '%s' % (
2443 '\n '.join(missing),
2444 cookie_auth.get_gitcookies_path(),
2445 cookie_auth.get_netrc_path(),
2446 cookie_auth.get_new_password_message(git_host)))
2447
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002448 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002449 if not self.GetIssue():
2450 return
2451
2452 # Warm change details cache now to avoid RPCs later, reducing latency for
2453 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002454 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002455 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002456
2457 status = self._GetChangeDetail()['status']
2458 if status in ('MERGED', 'ABANDONED'):
2459 DieWithError('Change %s has been %s, new uploads are not allowed' %
2460 (self.GetIssueURL(),
2461 'submitted' if status == 'MERGED' else 'abandoned'))
2462
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002463 if gerrit_util.GceAuthenticator.is_gce():
2464 return
2465 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2466 self._GetGerritHost())
2467 if self.GetIssueOwner() == cookies_user:
2468 return
2469 logging.debug('change %s owner is %s, cookies user is %s',
2470 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002471 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002472 # so ask what Gerrit thinks of this user.
2473 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2474 if details['email'] == self.GetIssueOwner():
2475 return
2476 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002477 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002478 'as %s.\n'
2479 'Uploading may fail due to lack of permissions.' %
2480 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2481 confirm_or_exit(action='upload')
2482
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002483 def _PostUnsetIssueProperties(self):
2484 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002485 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002486
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002487 def GetGerritObjForPresubmit(self):
2488 return presubmit_support.GerritAccessor(self._GetGerritHost())
2489
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002490 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002491 """Apply a rough heuristic to give a simple summary of an issue's review
2492 or CQ status, assuming adherence to a common workflow.
2493
2494 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002495 * 'error' - error from review tool (including deleted issues)
2496 * 'unsent' - no reviewers added
2497 * 'waiting' - waiting for review
2498 * 'reply' - waiting for uploader to reply to review
2499 * 'lgtm' - Code-Review label has been set
2500 * 'commit' - in the commit queue
2501 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002502 """
2503 if not self.GetIssue():
2504 return None
2505
2506 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002507 data = self._GetChangeDetail([
2508 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002509 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002510 return 'error'
2511
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002512 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002513 return 'closed'
2514
Aaron Gable9ab38c62017-04-06 14:36:33 -07002515 if data['labels'].get('Commit-Queue', {}).get('approved'):
2516 # The section will have an "approved" subsection if anyone has voted
2517 # the maximum value on the label.
2518 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002519
Aaron Gable9ab38c62017-04-06 14:36:33 -07002520 if data['labels'].get('Code-Review', {}).get('approved'):
2521 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002522
2523 if not data.get('reviewers', {}).get('REVIEWER', []):
2524 return 'unsent'
2525
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002526 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002527 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2528 last_message_author = messages.pop().get('author', {})
2529 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002530 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2531 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002532 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002533 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002534 if last_message_author.get('_account_id') == owner:
2535 # Most recent message was by owner.
2536 return 'waiting'
2537 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002538 # Some reply from non-owner.
2539 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002540
2541 # Somehow there are no messages even though there are reviewers.
2542 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002543
2544 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002545 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002546 patchset = data['revisions'][data['current_revision']]['_number']
2547 self.SetPatchset(patchset)
2548 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002549
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002550 def FetchDescription(self, force=False):
2551 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2552 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002553 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002554 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002555
dsansomee2d6fd92016-09-08 00:10:47 -07002556 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002557 if gerrit_util.HasPendingChangeEdit(
2558 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002559 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002560 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002561 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002562 'unpublished edit. Either publish the edit in the Gerrit web UI '
2563 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002564
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002565 gerrit_util.DeletePendingChangeEdit(
2566 self._GetGerritHost(), self._GerritChangeIdentifier())
2567 gerrit_util.SetCommitMessage(
2568 self._GetGerritHost(), self._GerritChangeIdentifier(),
2569 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002570
Aaron Gable636b13f2017-07-14 10:42:48 -07002571 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002572 gerrit_util.SetReview(
2573 self._GetGerritHost(), self._GerritChangeIdentifier(),
2574 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002575
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002576 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002577 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002578 messages = self._GetChangeDetail(
2579 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2580 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002581 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002582
2583 # Build dictionary of file comments for easy access and sorting later.
2584 # {author+date: {path: {patchset: {line: url+message}}}}
2585 comments = collections.defaultdict(
2586 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2587 for path, line_comments in file_comments.iteritems():
2588 for comment in line_comments:
2589 if comment.get('tag', '').startswith('autogenerated'):
2590 continue
2591 key = (comment['author']['email'], comment['updated'])
2592 if comment.get('side', 'REVISION') == 'PARENT':
2593 patchset = 'Base'
2594 else:
2595 patchset = 'PS%d' % comment['patch_set']
2596 line = comment.get('line', 0)
2597 url = ('https://%s/c/%s/%s/%s#%s%s' %
2598 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2599 'b' if comment.get('side') == 'PARENT' else '',
2600 str(line) if line else ''))
2601 comments[key][path][patchset][line] = (url, comment['message'])
2602
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002603 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002604 for msg in messages:
2605 # Don't bother showing autogenerated messages.
2606 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2607 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002608 # Gerrit spits out nanoseconds.
2609 assert len(msg['date'].split('.')[-1]) == 9
2610 date = datetime.datetime.strptime(msg['date'][:-3],
2611 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002612 message = msg['message']
2613 key = (msg['author']['email'], msg['date'])
2614 if key in comments:
2615 message += '\n'
2616 for path, patchsets in sorted(comments.get(key, {}).items()):
2617 if readable:
2618 message += '\n%s' % path
2619 for patchset, lines in sorted(patchsets.items()):
2620 for line, (url, content) in sorted(lines.items()):
2621 if line:
2622 line_str = 'Line %d' % line
2623 path_str = '%s:%d:' % (path, line)
2624 else:
2625 line_str = 'File comment'
2626 path_str = '%s:0:' % path
2627 if readable:
2628 message += '\n %s, %s: %s' % (patchset, line_str, url)
2629 message += '\n %s\n' % content
2630 else:
2631 message += '\n%s ' % path_str
2632 message += '\n%s\n' % content
2633
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002634 summary.append(_CommentSummary(
2635 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002636 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002637 sender=msg['author']['email'],
2638 # These could be inferred from the text messages and correlated with
2639 # Code-Review label maximum, however this is not reliable.
2640 # Leaving as is until the need arises.
2641 approval=False,
2642 disapproval=False,
2643 ))
2644 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002645
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002646 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002647 gerrit_util.AbandonChange(
2648 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002649
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002650 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002651 gerrit_util.SubmitChange(
2652 self._GetGerritHost(), self._GerritChangeIdentifier(),
2653 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002654
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002655 def _GetChangeDetail(self, options=None, no_cache=False):
2656 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002657
2658 If fresh data is needed, set no_cache=True which will clear cache and
2659 thus new data will be fetched from Gerrit.
2660 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002661 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002662 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002663
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002664 # Optimization to avoid multiple RPCs:
2665 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2666 'CURRENT_COMMIT' not in options):
2667 options.append('CURRENT_COMMIT')
2668
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002669 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002670 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002671 options = [o.upper() for o in options]
2672
2673 # Check in cache first unless no_cache is True.
2674 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002675 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002676 else:
2677 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002678 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002679 # Assumption: data fetched before with extra options is suitable
2680 # for return for a smaller set of options.
2681 # For example, if we cached data for
2682 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2683 # and request is for options=[CURRENT_REVISION],
2684 # THEN we can return prior cached data.
2685 if options_set.issubset(cached_options_set):
2686 return data
2687
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002688 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002689 data = gerrit_util.GetChangeDetail(
2690 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002691 except gerrit_util.GerritError as e:
2692 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002693 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002694 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002695
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002696 self._detail_cache.setdefault(cache_key, []).append(
2697 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002698 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002699
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002700 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002701 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002702 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002703 data = gerrit_util.GetChangeCommit(
2704 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002705 except gerrit_util.GerritError as e:
2706 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002707 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002708 raise
agable32978d92016-11-01 12:55:02 -07002709 return data
2710
Olivier Robin75ee7252018-04-13 10:02:56 +02002711 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002712 if git_common.is_dirty_git_tree('land'):
2713 return 1
tandriid60367b2016-06-22 05:25:12 -07002714 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2715 if u'Commit-Queue' in detail.get('labels', {}):
2716 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002717 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2718 'which can test and land changes for you. '
2719 'Are you sure you wish to bypass it?\n',
2720 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002721
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002722 differs = True
tandriic4344b52016-08-29 06:04:54 -07002723 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002724 # Note: git diff outputs nothing if there is no diff.
2725 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002726 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002727 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002728 if detail['current_revision'] == last_upload:
2729 differs = False
2730 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002731 print('WARNING: Local branch contents differ from latest uploaded '
2732 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002733 if differs:
2734 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002735 confirm_or_exit(
2736 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2737 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002738 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002739 elif not bypass_hooks:
2740 hook_results = self.RunHook(
2741 committing=True,
2742 may_prompt=not force,
2743 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002744 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2745 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002746 if not hook_results.should_continue():
2747 return 1
2748
2749 self.SubmitIssue(wait_for_merge=True)
2750 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002751 links = self._GetChangeCommit().get('web_links', [])
2752 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002753 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002754 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002755 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002756 return 0
2757
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002758 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002759 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002760 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002761 assert not directory
2762 assert parsed_issue_arg.valid
2763
2764 self._changelist.issue = parsed_issue_arg.issue
2765
2766 if parsed_issue_arg.hostname:
2767 self._gerrit_host = parsed_issue_arg.hostname
2768 self._gerrit_server = 'https://%s' % self._gerrit_host
2769
tandriic2405f52016-10-10 08:13:15 -07002770 try:
2771 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002772 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002773 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002774
2775 if not parsed_issue_arg.patchset:
2776 # Use current revision by default.
2777 revision_info = detail['revisions'][detail['current_revision']]
2778 patchset = int(revision_info['_number'])
2779 else:
2780 patchset = parsed_issue_arg.patchset
2781 for revision_info in detail['revisions'].itervalues():
2782 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2783 break
2784 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002785 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002786 (parsed_issue_arg.patchset, self.GetIssue()))
2787
Aaron Gable697a91b2018-01-19 15:20:15 -08002788 remote_url = self._changelist.GetRemoteUrl()
2789 if remote_url.endswith('.git'):
2790 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002791 remote_url = remote_url.rstrip('/')
2792
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002793 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002794 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002795
2796 if remote_url != fetch_info['url']:
2797 DieWithError('Trying to patch a change from %s but this repo appears '
2798 'to be %s.' % (fetch_info['url'], remote_url))
2799
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002800 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002801
Aaron Gable62619a32017-06-16 08:22:09 -07002802 if force:
2803 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2804 print('Checked out commit for change %i patchset %i locally' %
2805 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002806 elif nocommit:
2807 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2808 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002809 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002810 RunGit(['cherry-pick', 'FETCH_HEAD'])
2811 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002812 (parsed_issue_arg.issue, patchset))
2813 print('Note: this created a local commit which does not have '
2814 'the same hash as the one uploaded for review. This will make '
2815 'uploading changes based on top of this branch difficult.\n'
2816 'If you want to do that, use "git cl patch --force" instead.')
2817
Stefan Zagerd08043c2017-10-12 12:07:02 -07002818 if self.GetBranch():
2819 self.SetIssue(parsed_issue_arg.issue)
2820 self.SetPatchset(patchset)
2821 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2822 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2823 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2824 else:
2825 print('WARNING: You are in detached HEAD state.\n'
2826 'The patch has been applied to your checkout, but you will not be '
2827 'able to upload a new patch set to the gerrit issue.\n'
2828 'Try using the \'-b\' option if you would like to work on a '
2829 'branch and/or upload a new patch set.')
2830
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002831 return 0
2832
2833 @staticmethod
2834 def ParseIssueURL(parsed_url):
2835 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2836 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002837 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2838 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002839 # Short urls like https://domain/<issue_number> can be used, but don't allow
2840 # specifying the patchset (you'd 404), but we allow that here.
2841 if parsed_url.path == '/':
2842 part = parsed_url.fragment
2843 else:
2844 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002845 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002846 if match:
2847 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002848 issue=int(match.group(3)),
2849 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002850 hostname=parsed_url.netloc,
2851 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002852 return None
2853
tandrii16e0b4e2016-06-07 10:34:28 -07002854 def _GerritCommitMsgHookCheck(self, offer_removal):
2855 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2856 if not os.path.exists(hook):
2857 return
2858 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2859 # custom developer made one.
2860 data = gclient_utils.FileRead(hook)
2861 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2862 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002863 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002864 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002865 'and may interfere with it in subtle ways.\n'
2866 'We recommend you remove the commit-msg hook.')
2867 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002868 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002869 gclient_utils.rm_file_or_tree(hook)
2870 print('Gerrit commit-msg hook removed.')
2871 else:
2872 print('OK, will keep Gerrit commit-msg hook in place.')
2873
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002874 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002875 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002876 if options.squash and options.no_squash:
2877 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002878
2879 if not options.squash and not options.no_squash:
2880 # Load default for user, repo, squash=true, in this order.
2881 options.squash = settings.GetSquashGerritUploads()
2882 elif options.no_squash:
2883 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002884
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002885 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002886 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002887
Aaron Gableb56ad332017-01-06 15:24:31 -08002888 # This may be None; default fallback value is determined in logic below.
2889 title = options.title
2890
Dominic Battre7d1c4842017-10-27 09:17:28 +02002891 # Extract bug number from branch name.
2892 bug = options.bug
2893 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2894 if not bug and match:
2895 bug = match.group(1)
2896
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002897 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002898 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002899 if self.GetIssue():
2900 # Try to get the message from a previous upload.
2901 message = self.GetDescription()
2902 if not message:
2903 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002904 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002905 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002906 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002907 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002908 # When uploading a subsequent patchset, -m|--message is taken
2909 # as the patchset title if --title was not provided.
2910 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002911 else:
2912 default_title = RunGit(
2913 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002914 if options.force:
2915 title = default_title
2916 else:
2917 title = ask_for_data(
2918 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002919 change_id = self._GetChangeDetail()['change_id']
2920 while True:
2921 footer_change_ids = git_footers.get_footer_change_id(message)
2922 if footer_change_ids == [change_id]:
2923 break
2924 if not footer_change_ids:
2925 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002926 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002927 continue
2928 # There is already a valid footer but with different or several ids.
2929 # Doing this automatically is non-trivial as we don't want to lose
2930 # existing other footers, yet we want to append just 1 desired
2931 # Change-Id. Thus, just create a new footer, but let user verify the
2932 # new description.
2933 message = '%s\n\nChange-Id: %s' % (message, change_id)
2934 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002935 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002936 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002937 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002938 'Please, check the proposed correction to the description, '
2939 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2940 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2941 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002942 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002943 if not options.force:
2944 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002945 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002946 message = change_desc.description
2947 if not message:
2948 DieWithError("Description is empty. Aborting...")
2949 # Continue the while loop.
2950 # Sanity check of this code - we should end up with proper message
2951 # footer.
2952 assert [change_id] == git_footers.get_footer_change_id(message)
2953 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002954 else: # if not self.GetIssue()
2955 if options.message:
2956 message = options.message
2957 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002958 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002959 if options.title:
2960 message = options.title + '\n\n' + message
2961 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002962
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002963 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002964 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002965 # On first upload, patchset title is always this string, while
2966 # --title flag gets converted to first line of message.
2967 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002968 if not change_desc.description:
2969 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002970 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002971 if len(change_ids) > 1:
2972 DieWithError('too many Change-Id footers, at most 1 allowed.')
2973 if not change_ids:
2974 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002975 change_desc.set_description(git_footers.add_footer_change_id(
2976 change_desc.description,
2977 GenerateGerritChangeId(change_desc.description)))
2978 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002979 assert len(change_ids) == 1
2980 change_id = change_ids[0]
2981
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002982 if options.reviewers or options.tbrs or options.add_owners_to:
2983 change_desc.update_reviewers(options.reviewers, options.tbrs,
2984 options.add_owners_to, change)
2985
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002986 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002987 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2988 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002989 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002990 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2991 desc_tempfile.write(change_desc.description)
2992 desc_tempfile.close()
2993 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2994 '-F', desc_tempfile.name]).strip()
2995 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002996 else:
2997 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002998 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002999 if not change_desc.description:
3000 DieWithError("Description is empty. Aborting...")
3001
3002 if not git_footers.get_footer_change_id(change_desc.description):
3003 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003004 change_desc.set_description(
3005 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003006 if options.reviewers or options.tbrs or options.add_owners_to:
3007 change_desc.update_reviewers(options.reviewers, options.tbrs,
3008 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003009 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003010 # For no-squash mode, we assume the remote called "origin" is the one we
3011 # want. It is not worthwhile to support different workflows for
3012 # no-squash mode.
3013 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003014 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3015
3016 assert change_desc
3017 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:]))