blob: b4b8b65a02a07a2afbb0085b4c85974305ee8706 [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
Aiden Bennerc08566e2018-10-03 17:52:42 +000084# File name for yapf style config files.
85YAPF_CONFIG_FILENAME = '.style.yapf'
86
borenet6c0efe62016-10-19 08:13:29 -070087# Buildbucket master name prefix.
88MASTER_PREFIX = 'master.'
89
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000090# Shortcut since it quickly becomes redundant.
91Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000092
maruel@chromium.orgddd59412011-11-30 14:20:38 +000093# Initialized in main()
94settings = None
95
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010096# Used by tests/git_cl_test.py to add extra logging.
97# Inside the weirdly failing test, add this:
98# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -070099# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100100_IS_BEING_TESTED = False
101
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000102
Christopher Lamf732cd52017-01-24 12:40:11 +1100103def DieWithError(message, change_desc=None):
104 if change_desc:
105 SaveDescriptionBackup(change_desc)
106
vapiera7fbd5a2016-06-16 09:17:49 -0700107 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108 sys.exit(1)
109
110
Christopher Lamf732cd52017-01-24 12:40:11 +1100111def SaveDescriptionBackup(change_desc):
112 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000113 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100114 backup_file = open(backup_path, 'w')
115 backup_file.write(change_desc.description)
116 backup_file.close()
117
118
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000119def GetNoGitPagerEnv():
120 env = os.environ.copy()
121 # 'cat' is a magical git string that disables pagers on all platforms.
122 env['GIT_PAGER'] = 'cat'
123 return env
124
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000125
bsep@chromium.org627d9002016-04-29 00:00:52 +0000126def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000127 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000128 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000129 except subprocess2.CalledProcessError as e:
130 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000131 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000132 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000133 'Command "%s" failed.\n%s' % (
134 ' '.join(args), error_message or e.stdout or ''))
135 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000136
137
138def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000139 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000140 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000141
142
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000143def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000144 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700145 if suppress_stderr:
146 stderr = subprocess2.VOID
147 else:
148 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000149 try:
tandrii5d48c322016-08-18 16:19:37 -0700150 (out, _), code = subprocess2.communicate(['git'] + args,
151 env=GetNoGitPagerEnv(),
152 stdout=subprocess2.PIPE,
153 stderr=stderr)
154 return code, out
155 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900156 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700157 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000158
159
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000160def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000161 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000162 return RunGitWithCode(args, suppress_stderr=True)[1]
163
164
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000165def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000166 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000167 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000168 return (version.startswith(prefix) and
169 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000170
171
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000172def BranchExists(branch):
173 """Return True if specified branch exists."""
174 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
175 suppress_stderr=True)
176 return not code
177
178
tandrii2a16b952016-10-19 07:09:44 -0700179def time_sleep(seconds):
180 # Use this so that it can be mocked in tests without interfering with python
181 # system machinery.
182 import time # Local import to discourage others from importing time globally.
183 return time.sleep(seconds)
184
185
maruel@chromium.org90541732011-04-01 17:54:18 +0000186def ask_for_data(prompt):
187 try:
188 return raw_input(prompt)
189 except KeyboardInterrupt:
190 # Hide the exception.
191 sys.exit(1)
192
193
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100194def confirm_or_exit(prefix='', action='confirm'):
195 """Asks user to press enter to continue or press Ctrl+C to abort."""
196 if not prefix or prefix.endswith('\n'):
197 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100198 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100199 mid = ' Press'
200 elif prefix.endswith(' '):
201 mid = 'press'
202 else:
203 mid = ' press'
204 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
205
206
207def ask_for_explicit_yes(prompt):
208 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
209 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
210 while True:
211 if 'yes'.startswith(result):
212 return True
213 if 'no'.startswith(result):
214 return False
215 result = ask_for_data('Please, type yes or no: ').lower()
216
217
tandrii5d48c322016-08-18 16:19:37 -0700218def _git_branch_config_key(branch, key):
219 """Helper method to return Git config key for a branch."""
220 assert branch, 'branch name is required to set git config for it'
221 return 'branch.%s.%s' % (branch, key)
222
223
224def _git_get_branch_config_value(key, default=None, value_type=str,
225 branch=False):
226 """Returns git config value of given or current branch if any.
227
228 Returns default in all other cases.
229 """
230 assert value_type in (int, str, bool)
231 if branch is False: # Distinguishing default arg value from None.
232 branch = GetCurrentBranch()
233
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000234 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700235 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000236
tandrii5d48c322016-08-18 16:19:37 -0700237 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700238 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700239 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700240 # git config also has --int, but apparently git config suffers from integer
241 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700242 args.append(_git_branch_config_key(branch, key))
243 code, out = RunGitWithCode(args)
244 if code == 0:
245 value = out.strip()
246 if value_type == int:
247 return int(value)
248 if value_type == bool:
249 return bool(value.lower() == 'true')
250 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 return default
252
253
tandrii5d48c322016-08-18 16:19:37 -0700254def _git_set_branch_config_value(key, value, branch=None, **kwargs):
255 """Sets the value or unsets if it's None of a git branch config.
256
257 Valid, though not necessarily existing, branch must be provided,
258 otherwise currently checked out branch is used.
259 """
260 if not branch:
261 branch = GetCurrentBranch()
262 assert branch, 'a branch name OR currently checked out branch is required'
263 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700264 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700265 if value is None:
266 args.append('--unset')
267 elif isinstance(value, bool):
268 args.append('--bool')
269 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700270 else:
tandrii33a46ff2016-08-23 05:53:40 -0700271 # git config also has --int, but apparently git config suffers from integer
272 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700273 value = str(value)
274 args.append(_git_branch_config_key(branch, key))
275 if value is not None:
276 args.append(value)
277 RunGit(args, **kwargs)
278
279
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100280def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700281 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100282
283 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
284 """
285 # Git also stores timezone offset, but it only affects visual display,
286 # actual point in time is defined by this timestamp only.
287 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
288
289
290def _git_amend_head(message, committer_timestamp):
291 """Amends commit with new message and desired committer_timestamp.
292
293 Sets committer timezone to UTC.
294 """
295 env = os.environ.copy()
296 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
297 return RunGit(['commit', '--amend', '-m', message], env=env)
298
299
machenbach@chromium.org45453142015-09-15 08:45:22 +0000300def _get_properties_from_options(options):
301 properties = dict(x.split('=', 1) for x in options.properties)
302 for key, val in properties.iteritems():
303 try:
304 properties[key] = json.loads(val)
305 except ValueError:
306 pass # If a value couldn't be evaluated, treat it as a string.
307 return properties
308
309
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000310def _prefix_master(master):
311 """Convert user-specified master name to full master name.
312
313 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
314 name, while the developers always use shortened master name
315 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
316 function does the conversion for buildbucket migration.
317 """
borenet6c0efe62016-10-19 08:13:29 -0700318 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000319 return master
borenet6c0efe62016-10-19 08:13:29 -0700320 return '%s%s' % (MASTER_PREFIX, master)
321
322
323def _unprefix_master(bucket):
324 """Convert bucket name to shortened master name.
325
326 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
327 name, while the developers always use shortened master name
328 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
329 function does the conversion for buildbucket migration.
330 """
331 if bucket.startswith(MASTER_PREFIX):
332 return bucket[len(MASTER_PREFIX):]
333 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000334
335
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000336def _buildbucket_retry(operation_name, http, *args, **kwargs):
337 """Retries requests to buildbucket service and returns parsed json content."""
338 try_count = 0
339 while True:
340 response, content = http.request(*args, **kwargs)
341 try:
342 content_json = json.loads(content)
343 except ValueError:
344 content_json = None
345
346 # Buildbucket could return an error even if status==200.
347 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000348 error = content_json.get('error')
349 if error.get('code') == 403:
350 raise BuildbucketResponseException(
351 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000352 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000353 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000354 raise BuildbucketResponseException(msg)
355
356 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700357 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 raise BuildbucketResponseException(
359 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700360 'Please file bugs at http://crbug.com, '
361 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 content)
363 return content_json
364 if response.status < 500 or try_count >= 2:
365 raise httplib2.HttpLib2Error(content)
366
367 # status >= 500 means transient failures.
368 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700369 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000370 try_count += 1
371 assert False, 'unreachable'
372
373
qyearsley1fdfcb62016-10-24 13:22:03 -0700374def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700375 """Returns a dict mapping bucket names to builders and tests,
376 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700377 """
qyearsleydd49f942016-10-28 11:57:22 -0700378 # If no bots are listed, we try to get a set of builders and tests based
379 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700380 if not options.bot:
381 change = changelist.GetChange(
382 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700383 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700384 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700385 change=change,
386 changed_files=change.LocalPaths(),
387 repository_root=settings.GetRoot(),
388 default_presubmit=None,
389 project=None,
390 verbose=options.verbose,
391 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700392 if masters is None:
393 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100394 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700395
qyearsley1fdfcb62016-10-24 13:22:03 -0700396 if options.bucket:
397 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700398 if options.master:
399 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700400
qyearsleydd49f942016-10-28 11:57:22 -0700401 # If bots are listed but no master or bucket, then we need to find out
402 # the corresponding master for each bot.
403 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
404 if error_message:
405 option_parser.error(
406 'Tryserver master cannot be found because: %s\n'
407 'Please manually specify the tryserver master, e.g. '
408 '"-m tryserver.chromium.linux".' % error_message)
409 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700410
411
qyearsley123a4682016-10-26 09:12:17 -0700412def _get_bucket_map_for_builders(builders):
413 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700414 map_url = 'https://builders-map.appspot.com/'
415 try:
qyearsley123a4682016-10-26 09:12:17 -0700416 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700417 except urllib2.URLError as e:
418 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
419 (map_url, e))
420 except ValueError as e:
421 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700422 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700423 return None, 'Failed to build master map.'
424
qyearsley123a4682016-10-26 09:12:17 -0700425 bucket_map = {}
426 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800427 bucket = builders_map.get(builder, {}).get('bucket')
428 if bucket:
429 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700430 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700431
432
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800433def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700434 """Sends a request to Buildbucket to trigger try jobs for a changelist.
435
436 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700437 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700438 changelist: Changelist that the try jobs are associated with.
439 buckets: A nested dict mapping bucket names to builders to tests.
440 options: Command-line options.
441 """
tandriide281ae2016-10-12 06:02:30 -0700442 assert changelist.GetIssue(), 'CL must be uploaded first'
443 codereview_url = changelist.GetCodereviewServer()
444 assert codereview_url, 'CL must be uploaded first'
445 patchset = patchset or changelist.GetMostRecentPatchset()
446 assert patchset, 'CL must be uploaded first'
447
448 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700449 # Cache the buildbucket credentials under the codereview host key, so that
450 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700451 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000452 http = authenticator.authorize(httplib2.Http())
453 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700454
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000455 buildbucket_put_url = (
456 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000457 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700458 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
459 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
460 hostname=codereview_host,
461 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000462 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700463
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700464 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800465 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700466 if options.clobber:
467 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700468 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700469 if extra_properties:
470 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000471
472 batch_req_body = {'builds': []}
473 print_text = []
474 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700475 for bucket, builders_and_tests in sorted(buckets.iteritems()):
476 print_text.append('Bucket: %s' % bucket)
477 master = None
478 if bucket.startswith(MASTER_PREFIX):
479 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000480 for builder, tests in sorted(builders_and_tests.iteritems()):
481 print_text.append(' %s: %s' % (builder, tests))
482 parameters = {
483 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000484 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100485 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000486 'revision': options.revision,
487 }],
tandrii8c5a3532016-11-04 07:52:02 -0700488 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000489 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000490 if 'presubmit' in builder.lower():
491 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000492 if tests:
493 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700494
495 tags = [
496 'builder:%s' % builder,
497 'buildset:%s' % buildset,
498 'user_agent:git_cl_try',
499 ]
500 if master:
501 parameters['properties']['master'] = master
502 tags.append('master:%s' % master)
503
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000504 batch_req_body['builds'].append(
505 {
506 'bucket': bucket,
507 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000508 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700509 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000510 }
511 )
512
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000513 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700514 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000515 http,
516 buildbucket_put_url,
517 'PUT',
518 body=json.dumps(batch_req_body),
519 headers={'Content-Type': 'application/json'}
520 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000521 print_text.append('To see results here, run: git cl try-results')
522 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700523 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000524
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000525
tandrii221ab252016-10-06 08:12:04 -0700526def fetch_try_jobs(auth_config, changelist, buildbucket_host,
527 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700528 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000529
qyearsley53f48a12016-09-01 10:45:13 -0700530 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000531 """
tandrii221ab252016-10-06 08:12:04 -0700532 assert buildbucket_host
533 assert changelist.GetIssue(), 'CL must be uploaded first'
534 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
535 patchset = patchset or changelist.GetMostRecentPatchset()
536 assert patchset, 'CL must be uploaded first'
537
538 codereview_url = changelist.GetCodereviewServer()
539 codereview_host = urlparse.urlparse(codereview_url).hostname
540 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000541 if authenticator.has_cached_credentials():
542 http = authenticator.authorize(httplib2.Http())
543 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700544 print('Warning: Some results might be missing because %s' %
545 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700546 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000547 http = httplib2.Http()
548
549 http.force_exception_to_status_code = True
550
tandrii221ab252016-10-06 08:12:04 -0700551 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
552 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
553 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000554 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700555 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 params = {'tag': 'buildset:%s' % buildset}
557
558 builds = {}
559 while True:
560 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700561 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700563 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564 for build in content.get('builds', []):
565 builds[build['id']] = build
566 if 'next_cursor' in content:
567 params['start_cursor'] = content['next_cursor']
568 else:
569 break
570 return builds
571
572
qyearsleyeab3c042016-08-24 09:18:28 -0700573def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000574 """Prints nicely result of fetch_try_jobs."""
575 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700576 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577 return
578
579 # Make a copy, because we'll be modifying builds dictionary.
580 builds = builds.copy()
581 builder_names_cache = {}
582
583 def get_builder(b):
584 try:
585 return builder_names_cache[b['id']]
586 except KeyError:
587 try:
588 parameters = json.loads(b['parameters_json'])
589 name = parameters['builder_name']
590 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700591 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700592 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000593 name = None
594 builder_names_cache[b['id']] = name
595 return name
596
597 def get_bucket(b):
598 bucket = b['bucket']
599 if bucket.startswith('master.'):
600 return bucket[len('master.'):]
601 return bucket
602
603 if options.print_master:
604 name_fmt = '%%-%ds %%-%ds' % (
605 max(len(str(get_bucket(b))) for b in builds.itervalues()),
606 max(len(str(get_builder(b))) for b in builds.itervalues()))
607 def get_name(b):
608 return name_fmt % (get_bucket(b), get_builder(b))
609 else:
610 name_fmt = '%%-%ds' % (
611 max(len(str(get_builder(b))) for b in builds.itervalues()))
612 def get_name(b):
613 return name_fmt % get_builder(b)
614
615 def sort_key(b):
616 return b['status'], b.get('result'), get_name(b), b.get('url')
617
618 def pop(title, f, color=None, **kwargs):
619 """Pop matching builds from `builds` dict and print them."""
620
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000621 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000622 colorize = str
623 else:
624 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
625
626 result = []
627 for b in builds.values():
628 if all(b.get(k) == v for k, v in kwargs.iteritems()):
629 builds.pop(b['id'])
630 result.append(b)
631 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700632 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000633 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700634 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000635
636 total = len(builds)
637 pop(status='COMPLETED', result='SUCCESS',
638 title='Successes:', color=Fore.GREEN,
639 f=lambda b: (get_name(b), b.get('url')))
640 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
641 title='Infra Failures:', color=Fore.MAGENTA,
642 f=lambda b: (get_name(b), b.get('url')))
643 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
644 title='Failures:', color=Fore.RED,
645 f=lambda b: (get_name(b), b.get('url')))
646 pop(status='COMPLETED', result='CANCELED',
647 title='Canceled:', color=Fore.MAGENTA,
648 f=lambda b: (get_name(b),))
649 pop(status='COMPLETED', result='FAILURE',
650 failure_reason='INVALID_BUILD_DEFINITION',
651 title='Wrong master/builder name:', color=Fore.MAGENTA,
652 f=lambda b: (get_name(b),))
653 pop(status='COMPLETED', result='FAILURE',
654 title='Other failures:',
655 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
656 pop(status='COMPLETED',
657 title='Other finished:',
658 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
659 pop(status='STARTED',
660 title='Started:', color=Fore.YELLOW,
661 f=lambda b: (get_name(b), b.get('url')))
662 pop(status='SCHEDULED',
663 title='Scheduled:',
664 f=lambda b: (get_name(b), 'id=%s' % b['id']))
665 # The last section is just in case buildbucket API changes OR there is a bug.
666 pop(title='Other:',
667 f=lambda b: (get_name(b), 'id=%s' % b['id']))
668 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700669 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000670
671
Aiden Bennerc08566e2018-10-03 17:52:42 +0000672def _ComputeDiffLineRanges(files, upstream_commit):
673 """Gets the changed line ranges for each file since upstream_commit.
674
675 Parses a git diff on provided files and returns a dict that maps a file name
676 to an ordered list of range tuples in the form (start_line, count).
677 Ranges are in the same format as a git diff.
678 """
679 # If files is empty then diff_output will be a full diff.
680 if len(files) == 0:
681 return {}
682
683 # Take diff and find the line ranges where there are changes.
684 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
685 diff_output = RunGit(diff_cmd)
686
687 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
688 # 2 capture groups
689 # 0 == fname of diff file
690 # 1 == 'diff_start,diff_count' or 'diff_start'
691 # will match each of
692 # diff --git a/foo.foo b/foo.py
693 # @@ -12,2 +14,3 @@
694 # @@ -12,2 +17 @@
695 # running re.findall on the above string with pattern will give
696 # [('foo.py', ''), ('', '14,3'), ('', '17')]
697
698 curr_file = None
699 line_diffs = {}
700 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
701 if match[0] != '':
702 # Will match the second filename in diff --git a/a.py b/b.py.
703 curr_file = match[0]
704 line_diffs[curr_file] = []
705 else:
706 # Matches +14,3
707 if ',' in match[1]:
708 diff_start, diff_count = match[1].split(',')
709 else:
710 # Single line changes are of the form +12 instead of +12,1.
711 diff_start = match[1]
712 diff_count = 1
713
714 diff_start = int(diff_start)
715 diff_count = int(diff_count)
716
717 # If diff_count == 0 this is a removal we can ignore.
718 line_diffs[curr_file].append((diff_start, diff_count))
719
720 return line_diffs
721
722
723def _FindYapfConfigFile(fpath,
724 yapf_config_cache,
725 top_dir=None,
726 default_style=None):
727 """Checks if a yapf file is in any parent directory of fpath until top_dir.
728
729 Recursively checks parent directories to find yapf file
730 and if no yapf file is found returns default_style.
731 Uses yapf_config_cache as a cache for previously found files.
732 """
733 # Return result if we've already computed it.
734 if fpath in yapf_config_cache:
735 return yapf_config_cache[fpath]
736
737 # Check if there is a style file in the current directory.
738 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
739 dirname = os.path.dirname(fpath)
740 if os.path.isfile(yapf_file):
741 ret = yapf_file
742 elif fpath == top_dir or dirname == fpath:
743 # If we're at the top level directory, or if we're at root
744 # use the chromium default yapf style.
745 ret = default_style
746 else:
747 # Otherwise recurse on the current directory.
748 ret = _FindYapfConfigFile(dirname, yapf_config_cache, top_dir,
749 default_style)
750 yapf_config_cache[fpath] = ret
751 return ret
752
753
qyearsley53f48a12016-09-01 10:45:13 -0700754def write_try_results_json(output_file, builds):
755 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
756
757 The input |builds| dict is assumed to be generated by Buildbucket.
758 Buildbucket documentation: http://goo.gl/G0s101
759 """
760
761 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800762 """Extracts some of the information from one build dict."""
763 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700764 return {
765 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700766 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800767 'builder_name': parameters.get('builder_name'),
768 'created_ts': build.get('created_ts'),
769 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700770 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800771 'result': build.get('result'),
772 'status': build.get('status'),
773 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700774 'url': build.get('url'),
775 }
776
777 converted = []
778 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000779 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700780 write_json(output_file, converted)
781
782
Aaron Gable13101a62018-02-09 13:20:41 -0800783def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000784 """Prints statistics about the change to the user."""
785 # --no-ext-diff is broken in some versions of Git, so try to work around
786 # this by overriding the environment (but there is still a problem if the
787 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000788 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000789 if 'GIT_EXTERNAL_DIFF' in env:
790 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000791
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000792 try:
793 stdout = sys.stdout.fileno()
794 except AttributeError:
795 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000796 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800797 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000798 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000799
800
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000801class BuildbucketResponseException(Exception):
802 pass
803
804
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805class Settings(object):
806 def __init__(self):
807 self.default_server = None
808 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000809 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 self.tree_status_url = None
811 self.viewvc_url = None
812 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000813 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000814 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000815 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000816 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000817 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000818 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819
820 def LazyUpdateIfNeeded(self):
821 """Updates the settings from a codereview.settings file, if available."""
822 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000823 # The only value that actually changes the behavior is
824 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000825 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000826 error_ok=True
827 ).strip().lower()
828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000830 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000831 LoadCodereviewSettingsFromFile(cr_settings_file)
832 self.updated = True
833
834 def GetDefaultServerUrl(self, error_ok=False):
835 if not self.default_server:
836 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000837 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000838 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839 if error_ok:
840 return self.default_server
841 if not self.default_server:
842 error_message = ('Could not find settings file. You must configure '
843 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000844 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000845 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846 return self.default_server
847
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000848 @staticmethod
849 def GetRelativeRoot():
850 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000851
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000853 if self.root is None:
854 self.root = os.path.abspath(self.GetRelativeRoot())
855 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000856
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000857 def GetGitMirror(self, remote='origin'):
858 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000859 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000860 if not os.path.isdir(local_url):
861 return None
862 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
863 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100864 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100865 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000866 if mirror.exists():
867 return mirror
868 return None
869
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870 def GetTreeStatusUrl(self, error_ok=False):
871 if not self.tree_status_url:
872 error_message = ('You must configure your tree status URL by running '
873 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000874 self.tree_status_url = self._GetRietveldConfig(
875 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876 return self.tree_status_url
877
878 def GetViewVCUrl(self):
879 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000880 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881 return self.viewvc_url
882
rmistry@google.com90752582014-01-14 21:04:50 +0000883 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000884 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000885
rmistry@google.com78948ed2015-07-08 23:09:57 +0000886 def GetIsSkipDependencyUpload(self, branch_name):
887 """Returns true if specified branch should skip dep uploads."""
888 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
889 error_ok=True)
890
rmistry@google.com5626a922015-02-26 14:03:30 +0000891 def GetRunPostUploadHook(self):
892 run_post_upload_hook = self._GetRietveldConfig(
893 'run-post-upload-hook', error_ok=True)
894 return run_post_upload_hook == "True"
895
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000896 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000897 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000898
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000899 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000900 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000901
ukai@chromium.orge8077812012-02-03 03:41:46 +0000902 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700903 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000904 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700905 self.is_gerrit = (
906 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000907 return self.is_gerrit
908
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000909 def GetSquashGerritUploads(self):
910 """Return true if uploads to Gerrit should be squashed by default."""
911 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700912 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
913 if self.squash_gerrit_uploads is None:
914 # Default is squash now (http://crbug.com/611892#c23).
915 self.squash_gerrit_uploads = not (
916 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
917 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000918 return self.squash_gerrit_uploads
919
tandriia60502f2016-06-20 02:01:53 -0700920 def GetSquashGerritUploadsOverride(self):
921 """Return True or False if codereview.settings should be overridden.
922
923 Returns None if no override has been defined.
924 """
925 # See also http://crbug.com/611892#c23
926 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
927 error_ok=True).strip()
928 if result == 'true':
929 return True
930 if result == 'false':
931 return False
932 return None
933
tandrii@chromium.org28253532016-04-14 13:46:56 +0000934 def GetGerritSkipEnsureAuthenticated(self):
935 """Return True if EnsureAuthenticated should not be done for Gerrit
936 uploads."""
937 if self.gerrit_skip_ensure_authenticated is None:
938 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000939 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000940 error_ok=True).strip() == 'true')
941 return self.gerrit_skip_ensure_authenticated
942
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000943 def GetGitEditor(self):
944 """Return the editor specified in the git config, or None if none is."""
945 if self.git_editor is None:
946 self.git_editor = self._GetConfig('core.editor', error_ok=True)
947 return self.git_editor or None
948
thestig@chromium.org44202a22014-03-11 19:22:18 +0000949 def GetLintRegex(self):
950 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
951 DEFAULT_LINT_REGEX)
952
953 def GetLintIgnoreRegex(self):
954 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
955 DEFAULT_LINT_IGNORE_REGEX)
956
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000957 def GetProject(self):
958 if not self.project:
959 self.project = self._GetRietveldConfig('project', error_ok=True)
960 return self.project
961
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000962 def _GetRietveldConfig(self, param, **kwargs):
963 return self._GetConfig('rietveld.' + param, **kwargs)
964
rmistry@google.com78948ed2015-07-08 23:09:57 +0000965 def _GetBranchConfig(self, branch_name, param, **kwargs):
966 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
967
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968 def _GetConfig(self, param, **kwargs):
969 self.LazyUpdateIfNeeded()
970 return RunGit(['config', param], **kwargs).strip()
971
972
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100973@contextlib.contextmanager
974def _get_gerrit_project_config_file(remote_url):
975 """Context manager to fetch and store Gerrit's project.config from
976 refs/meta/config branch and store it in temp file.
977
978 Provides a temporary filename or None if there was error.
979 """
980 error, _ = RunGitWithCode([
981 'fetch', remote_url,
982 '+refs/meta/config:refs/git_cl/meta/config'])
983 if error:
984 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700985 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100986 (remote_url, error))
987 yield None
988 return
989
990 error, project_config_data = RunGitWithCode(
991 ['show', 'refs/git_cl/meta/config:project.config'])
992 if error:
993 print('WARNING: project.config file not found')
994 yield None
995 return
996
997 with gclient_utils.temporary_directory() as tempdir:
998 project_config_file = os.path.join(tempdir, 'project.config')
999 gclient_utils.FileWrite(project_config_file, project_config_data)
1000 yield project_config_file
1001
1002
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001003def ShortBranchName(branch):
1004 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001005 return branch.replace('refs/heads/', '', 1)
1006
1007
1008def GetCurrentBranchRef():
1009 """Returns branch ref (e.g., refs/heads/master) or None."""
1010 return RunGit(['symbolic-ref', 'HEAD'],
1011 stderr=subprocess2.VOID, error_ok=True).strip() or None
1012
1013
1014def GetCurrentBranch():
1015 """Returns current branch or None.
1016
1017 For refs/heads/* branches, returns just last part. For others, full ref.
1018 """
1019 branchref = GetCurrentBranchRef()
1020 if branchref:
1021 return ShortBranchName(branchref)
1022 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023
1024
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001025class _CQState(object):
1026 """Enum for states of CL with respect to Commit Queue."""
1027 NONE = 'none'
1028 DRY_RUN = 'dry_run'
1029 COMMIT = 'commit'
1030
1031 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1032
1033
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001034class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001035 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001036 self.issue = issue
1037 self.patchset = patchset
1038 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001039 assert codereview in (None, 'rietveld', 'gerrit')
1040 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001041
1042 @property
1043 def valid(self):
1044 return self.issue is not None
1045
1046
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001047def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001048 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1049 fail_result = _ParsedIssueNumberArgument()
1050
1051 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001052 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001053 if not arg.startswith('http'):
1054 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001055
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001056 url = gclient_utils.UpgradeToHttps(arg)
1057 try:
1058 parsed_url = urlparse.urlparse(url)
1059 except ValueError:
1060 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001061
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001062 if codereview is not None:
1063 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1064 return parsed or fail_result
1065
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001066 results = {}
1067 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1068 parsed = cls.ParseIssueURL(parsed_url)
1069 if parsed is not None:
1070 results[name] = parsed
1071
1072 if not results:
1073 return fail_result
1074 if len(results) == 1:
1075 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001076
1077 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1078 # This is likely Gerrit.
1079 return results['gerrit']
1080 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001081 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001082
1083
Aaron Gablea45ee112016-11-22 15:14:38 -08001084class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001085 def __init__(self, issue, url):
1086 self.issue = issue
1087 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001088 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001089
1090 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001091 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001092 self.issue, self.url)
1093
1094
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001095_CommentSummary = collections.namedtuple(
1096 '_CommentSummary', ['date', 'message', 'sender',
1097 # TODO(tandrii): these two aren't known in Gerrit.
1098 'approval', 'disapproval'])
1099
1100
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001102 """Changelist works with one changelist in local branch.
1103
1104 Supports two codereview backends: Rietveld or Gerrit, selected at object
1105 creation.
1106
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001107 Notes:
1108 * Not safe for concurrent multi-{thread,process} use.
1109 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001110 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001111 """
1112
1113 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1114 """Create a new ChangeList instance.
1115
1116 If issue is given, the codereview must be given too.
1117
1118 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1119 Otherwise, it's decided based on current configuration of the local branch,
1120 with default being 'rietveld' for backwards compatibility.
1121 See _load_codereview_impl for more details.
1122
1123 **kwargs will be passed directly to codereview implementation.
1124 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001126 global settings
1127 if not settings:
1128 # Happens when git_cl.py is used as a utility library.
1129 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001130
1131 if issue:
1132 assert codereview, 'codereview must be known, if issue is known'
1133
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 self.branchref = branchref
1135 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001136 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 self.branch = ShortBranchName(self.branchref)
1138 else:
1139 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001141 self.lookedup_issue = False
1142 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 self.has_description = False
1144 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001145 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001147 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001148 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001149 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001150 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001151
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001152 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001153 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001154 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001155 assert self._codereview_impl
1156 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001157
1158 def _load_codereview_impl(self, codereview=None, **kwargs):
1159 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001160 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1161 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1162 self._codereview = codereview
1163 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001164 return
1165
1166 # Automatic selection based on issue number set for a current branch.
1167 # Rietveld takes precedence over Gerrit.
1168 assert not self.issue
1169 # Whether we find issue or not, we are doing the lookup.
1170 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001171 if self.GetBranch():
1172 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1173 issue = _git_get_branch_config_value(
1174 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1175 if issue:
1176 self._codereview = codereview
1177 self._codereview_impl = cls(self, **kwargs)
1178 self.issue = int(issue)
1179 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001180
1181 # No issue is set for this branch, so decide based on repo-wide settings.
1182 return self._load_codereview_impl(
1183 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1184 **kwargs)
1185
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001186 def IsGerrit(self):
1187 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001188
1189 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001190 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001191
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001192 The return value is a string suitable for passing to git cl with the --cc
1193 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001194 """
1195 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001196 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001197 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001198 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1199 return self.cc
1200
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001201 def GetCCListWithoutDefault(self):
1202 """Return the users cc'd on this CL excluding default ones."""
1203 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001204 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001205 return self.cc
1206
Daniel Cheng7227d212017-11-17 08:12:37 -08001207 def ExtendCC(self, more_cc):
1208 """Extends the list of users to cc on this CL based on the changed files."""
1209 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210
1211 def GetBranch(self):
1212 """Returns the short branch name, e.g. 'master'."""
1213 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001215 if not branchref:
1216 return None
1217 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 self.branch = ShortBranchName(self.branchref)
1219 return self.branch
1220
1221 def GetBranchRef(self):
1222 """Returns the full branch name, e.g. 'refs/heads/master'."""
1223 self.GetBranch() # Poke the lazy loader.
1224 return self.branchref
1225
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001226 def ClearBranch(self):
1227 """Clears cached branch data of this object."""
1228 self.branch = self.branchref = None
1229
tandrii5d48c322016-08-18 16:19:37 -07001230 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1231 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1232 kwargs['branch'] = self.GetBranch()
1233 return _git_get_branch_config_value(key, default, **kwargs)
1234
1235 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1236 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1237 assert self.GetBranch(), (
1238 'this CL must have an associated branch to %sset %s%s' %
1239 ('un' if value is None else '',
1240 key,
1241 '' if value is None else ' to %r' % value))
1242 kwargs['branch'] = self.GetBranch()
1243 return _git_set_branch_config_value(key, value, **kwargs)
1244
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001245 @staticmethod
1246 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001247 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 e.g. 'origin', 'refs/heads/master'
1249 """
1250 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001251 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1252
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001254 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001256 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1257 error_ok=True).strip()
1258 if upstream_branch:
1259 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001261 # Else, try to guess the origin remote.
1262 remote_branches = RunGit(['branch', '-r']).split()
1263 if 'origin/master' in remote_branches:
1264 # Fall back on origin/master if it exits.
1265 remote = 'origin'
1266 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001268 DieWithError(
1269 'Unable to determine default branch to diff against.\n'
1270 'Either pass complete "git diff"-style arguments, like\n'
1271 ' git cl upload origin/master\n'
1272 'or verify this branch is set up to track another \n'
1273 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274
1275 return remote, upstream_branch
1276
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001277 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001278 upstream_branch = self.GetUpstreamBranch()
1279 if not BranchExists(upstream_branch):
1280 DieWithError('The upstream for the current branch (%s) does not exist '
1281 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001282 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001283 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001284
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285 def GetUpstreamBranch(self):
1286 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001289 upstream_branch = upstream_branch.replace('refs/heads/',
1290 'refs/remotes/%s/' % remote)
1291 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1292 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 self.upstream_branch = upstream_branch
1294 return self.upstream_branch
1295
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001296 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001297 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001298 remote, branch = None, self.GetBranch()
1299 seen_branches = set()
1300 while branch not in seen_branches:
1301 seen_branches.add(branch)
1302 remote, branch = self.FetchUpstreamTuple(branch)
1303 branch = ShortBranchName(branch)
1304 if remote != '.' or branch.startswith('refs/remotes'):
1305 break
1306 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001307 remotes = RunGit(['remote'], error_ok=True).split()
1308 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001309 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001310 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001311 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001312 logging.warn('Could not determine which remote this change is '
1313 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001314 else:
1315 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001316 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001317 branch = 'HEAD'
1318 if branch.startswith('refs/remotes'):
1319 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001320 elif branch.startswith('refs/branch-heads/'):
1321 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001322 else:
1323 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001324 return self._remote
1325
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001326 def GitSanityChecks(self, upstream_git_obj):
1327 """Checks git repo status and ensures diff is from local commits."""
1328
sbc@chromium.org79706062015-01-14 21:18:12 +00001329 if upstream_git_obj is None:
1330 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001331 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001332 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001333 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001334 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001335 return False
1336
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001337 # Verify the commit we're diffing against is in our current branch.
1338 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1339 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1340 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001341 print('ERROR: %s is not in the current branch. You may need to rebase '
1342 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001343 return False
1344
1345 # List the commits inside the diff, and verify they are all local.
1346 commits_in_diff = RunGit(
1347 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1348 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1349 remote_branch = remote_branch.strip()
1350 if code != 0:
1351 _, remote_branch = self.GetRemoteBranch()
1352
1353 commits_in_remote = RunGit(
1354 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1355
1356 common_commits = set(commits_in_diff) & set(commits_in_remote)
1357 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001358 print('ERROR: Your diff contains %d commits already in %s.\n'
1359 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1360 'the diff. If you are using a custom git flow, you can override'
1361 ' the reference used for this check with "git config '
1362 'gitcl.remotebranch <git-ref>".' % (
1363 len(common_commits), remote_branch, upstream_git_obj),
1364 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001365 return False
1366 return True
1367
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001368 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001369 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001370
1371 Returns None if it is not set.
1372 """
tandrii5d48c322016-08-18 16:19:37 -07001373 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001374
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 def GetRemoteUrl(self):
1376 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1377
1378 Returns None if there is no remote.
1379 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001380 is_cached, value = self._cached_remote_url
1381 if is_cached:
1382 return value
1383
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001384 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001385 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1386
1387 # If URL is pointing to a local directory, it is probably a git cache.
1388 if os.path.isdir(url):
1389 url = RunGit(['config', 'remote.%s.url' % remote],
1390 error_ok=True,
1391 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001392 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001393 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001395 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001396 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001397 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001398 self.issue = self._GitGetBranchConfigValue(
1399 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001400 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 return self.issue
1402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 def GetIssueURL(self):
1404 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001405 issue = self.GetIssue()
1406 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001407 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001408 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001410 def GetDescription(self, pretty=False, force=False):
1411 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001413 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 self.has_description = True
1415 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001416 # Set width to 72 columns + 2 space indent.
1417 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001419 lines = self.description.splitlines()
1420 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421 return self.description
1422
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001423 def GetDescriptionFooters(self):
1424 """Returns (non_footer_lines, footers) for the commit message.
1425
1426 Returns:
1427 non_footer_lines (list(str)) - Simple list of description lines without
1428 any footer. The lines do not contain newlines, nor does the list contain
1429 the empty line between the message and the footers.
1430 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1431 [("Change-Id", "Ideadbeef...."), ...]
1432 """
1433 raw_description = self.GetDescription()
1434 msg_lines, _, footers = git_footers.split_footers(raw_description)
1435 if footers:
1436 msg_lines = msg_lines[:len(msg_lines)-1]
1437 return msg_lines, footers
1438
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001440 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001441 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001442 self.patchset = self._GitGetBranchConfigValue(
1443 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001444 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001445 return self.patchset
1446
1447 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001448 """Set this branch's patchset. If patchset=0, clears the patchset."""
1449 assert self.GetBranch()
1450 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001451 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001452 else:
1453 self.patchset = int(patchset)
1454 self._GitSetBranchConfigValue(
1455 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001457 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001458 """Set this branch's issue. If issue isn't given, clears the issue."""
1459 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001460 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001461 issue = int(issue)
1462 self._GitSetBranchConfigValue(
1463 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001464 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001465 codereview_server = self._codereview_impl.GetCodereviewServer()
1466 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001467 self._GitSetBranchConfigValue(
1468 self._codereview_impl.CodereviewServerConfigKey(),
1469 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470 else:
tandrii5d48c322016-08-18 16:19:37 -07001471 # Reset all of these just to be clean.
1472 reset_suffixes = [
1473 'last-upload-hash',
1474 self._codereview_impl.IssueConfigKey(),
1475 self._codereview_impl.PatchsetConfigKey(),
1476 self._codereview_impl.CodereviewServerConfigKey(),
1477 ] + self._PostUnsetIssueProperties()
1478 for prop in reset_suffixes:
1479 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001480 msg = RunGit(['log', '-1', '--format=%B']).strip()
1481 if msg and git_footers.get_footer_change_id(msg):
1482 print('WARNING: The change patched into this branch has a Change-Id. '
1483 'Removing it.')
1484 RunGit(['commit', '--amend', '-m',
1485 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001486 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001487 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001488
dnjba1b0f32016-09-02 12:37:42 -07001489 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001490 if not self.GitSanityChecks(upstream_branch):
1491 DieWithError('\nGit sanity check failure')
1492
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001493 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001494 if not root:
1495 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001496 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001497
1498 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001499 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001500 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001501 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001502 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001503 except subprocess2.CalledProcessError:
1504 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001505 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001506 'This branch probably doesn\'t exist anymore. To reset the\n'
1507 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001508 ' git branch --set-upstream-to origin/master %s\n'
1509 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001510 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001511
maruel@chromium.org52424302012-08-29 15:14:30 +00001512 issue = self.GetIssue()
1513 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001514 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001515 description = self.GetDescription()
1516 else:
1517 # If the change was never uploaded, use the log messages of all commits
1518 # up to the branch point, as git cl upload will prefill the description
1519 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001520 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1521 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001522
1523 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001524 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001525 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001526 name,
1527 description,
1528 absroot,
1529 files,
1530 issue,
1531 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001532 author,
1533 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001534
dsansomee2d6fd92016-09-08 00:10:47 -07001535 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001536 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001537 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001538 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001539
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001540 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1541 """Sets the description for this CL remotely.
1542
1543 You can get description_lines and footers with GetDescriptionFooters.
1544
1545 Args:
1546 description_lines (list(str)) - List of CL description lines without
1547 newline characters.
1548 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1549 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1550 `List-Of-Tokens`). It will be case-normalized so that each token is
1551 title-cased.
1552 """
1553 new_description = '\n'.join(description_lines)
1554 if footers:
1555 new_description += '\n'
1556 for k, v in footers:
1557 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1558 if not git_footers.FOOTER_PATTERN.match(foot):
1559 raise ValueError('Invalid footer %r' % foot)
1560 new_description += foot + '\n'
1561 self.UpdateDescription(new_description, force)
1562
Edward Lesmes8e282792018-04-03 18:50:29 -04001563 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001564 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1565 try:
1566 return presubmit_support.DoPresubmitChecks(change, committing,
1567 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1568 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001569 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1570 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001571 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001572 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001573
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001574 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1575 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001576 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1577 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001578 else:
1579 # Assume url.
1580 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1581 urlparse.urlparse(issue_arg))
1582 if not parsed_issue_arg or not parsed_issue_arg.valid:
1583 DieWithError('Failed to parse issue argument "%s". '
1584 'Must be an issue number or a valid URL.' % issue_arg)
1585 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001586 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001587
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001588 def CMDUpload(self, options, git_diff_args, orig_args):
1589 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001590 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001591 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001592 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001593 else:
1594 if self.GetBranch() is None:
1595 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1596
1597 # Default to diffing against common ancestor of upstream branch
1598 base_branch = self.GetCommonAncestorWithUpstream()
1599 git_diff_args = [base_branch, 'HEAD']
1600
Aaron Gablec4c40d12017-05-22 11:49:53 -07001601 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1602 if not self.IsGerrit() and not self.GetIssue():
1603 print('=====================================')
1604 print('NOTICE: Rietveld is being deprecated. '
1605 'You can upload changes to Gerrit with')
1606 print(' git cl upload --gerrit')
1607 print('or set Gerrit to be your default code review tool with')
1608 print(' git config gerrit.host true')
1609 print('=====================================')
1610
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001611 # Fast best-effort checks to abort before running potentially
1612 # expensive hooks if uploading is likely to fail anyway. Passing these
1613 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001614 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001615 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001616
1617 # Apply watchlists on upload.
1618 change = self.GetChange(base_branch, None)
1619 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1620 files = [f.LocalPath() for f in change.AffectedFiles()]
1621 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001622 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001623
1624 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001625 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001626 # Set the reviewer list now so that presubmit checks can access it.
1627 change_description = ChangeDescription(change.FullDescriptionText())
1628 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001629 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001630 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001631 change)
1632 change.SetDescriptionText(change_description.description)
1633 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001634 may_prompt=not options.force,
1635 verbose=options.verbose,
1636 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001637 if not hook_results.should_continue():
1638 return 1
1639 if not options.reviewers and hook_results.reviewers:
1640 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001641 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001642
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001643 # TODO(tandrii): Checking local patchset against remote patchset is only
1644 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1645 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001646 latest_patchset = self.GetMostRecentPatchset()
1647 local_patchset = self.GetPatchset()
1648 if (latest_patchset and local_patchset and
1649 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001650 print('The last upload made from this repository was patchset #%d but '
1651 'the most recent patchset on the server is #%d.'
1652 % (local_patchset, latest_patchset))
1653 print('Uploading will still work, but if you\'ve uploaded to this '
1654 'issue from another machine or branch the patch you\'re '
1655 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001656 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001657
Aaron Gable13101a62018-02-09 13:20:41 -08001658 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001659 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001660 if not ret:
Ravi Mistry31e7d562018-04-02 12:53:57 -04001661 if self.IsGerrit():
1662 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
1663 options.cq_dry_run);
1664 else:
1665 if options.use_commit_queue:
1666 self.SetCQState(_CQState.COMMIT)
1667 elif options.cq_dry_run:
1668 self.SetCQState(_CQState.DRY_RUN)
tandrii4d0545a2016-07-06 03:56:49 -07001669
tandrii5d48c322016-08-18 16:19:37 -07001670 _git_set_branch_config_value('last-upload-hash',
1671 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001672 # Run post upload hooks, if specified.
1673 if settings.GetRunPostUploadHook():
1674 presubmit_support.DoPostUploadExecuter(
1675 change,
1676 self,
1677 settings.GetRoot(),
1678 options.verbose,
1679 sys.stdout)
1680
1681 # Upload all dependencies if specified.
1682 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001683 print()
1684 print('--dependencies has been specified.')
1685 print('All dependent local branches will be re-uploaded.')
1686 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001687 # Remove the dependencies flag from args so that we do not end up in a
1688 # loop.
1689 orig_args.remove('--dependencies')
1690 ret = upload_branch_deps(self, orig_args)
1691 return ret
1692
Ravi Mistry31e7d562018-04-02 12:53:57 -04001693 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1694 """Sets labels on the change based on the provided flags.
1695
1696 Sets labels if issue is already uploaded and known, else returns without
1697 doing anything.
1698
1699 Args:
1700 enable_auto_submit: Sets Auto-Submit+1 on the change.
1701 use_commit_queue: Sets Commit-Queue+2 on the change.
1702 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1703 both use_commit_queue and cq_dry_run are true.
1704 """
1705 if not self.GetIssue():
1706 return
1707 try:
1708 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1709 cq_dry_run)
1710 return 0
1711 except KeyboardInterrupt:
1712 raise
1713 except:
1714 labels = []
1715 if enable_auto_submit:
1716 labels.append('Auto-Submit')
1717 if use_commit_queue or cq_dry_run:
1718 labels.append('Commit-Queue')
1719 print('WARNING: Failed to set label(s) on your change: %s\n'
1720 'Either:\n'
1721 ' * Your project does not have the above label(s),\n'
1722 ' * You don\'t have permission to set the above label(s),\n'
1723 ' * There\'s a bug in this code (see stack trace below).\n' %
1724 (', '.join(labels)))
1725 # Still raise exception so that stack trace is printed.
1726 raise
1727
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001728 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001729 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001730
1731 Issue must have been already uploaded and known.
1732 """
1733 assert new_state in _CQState.ALL_STATES
1734 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001735 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001736 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001737 return 0
1738 except KeyboardInterrupt:
1739 raise
1740 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001741 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001742 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001743 ' * Your project has no CQ,\n'
1744 ' * You don\'t have permission to change the CQ state,\n'
1745 ' * There\'s a bug in this code (see stack trace below).\n'
1746 'Consider specifying which bots to trigger manually or asking your '
1747 'project owners for permissions or contacting Chrome Infra at:\n'
1748 'https://www.chromium.org/infra\n\n' %
1749 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001750 # Still raise exception so that stack trace is printed.
1751 raise
1752
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753 # Forward methods to codereview specific implementation.
1754
Aaron Gable636b13f2017-07-14 10:42:48 -07001755 def AddComment(self, message, publish=None):
1756 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001757
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001758 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001759 """Returns list of _CommentSummary for each comment.
1760
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001761 args:
1762 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001763 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001764 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001765
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001766 def CloseIssue(self):
1767 return self._codereview_impl.CloseIssue()
1768
1769 def GetStatus(self):
1770 return self._codereview_impl.GetStatus()
1771
1772 def GetCodereviewServer(self):
1773 return self._codereview_impl.GetCodereviewServer()
1774
tandriide281ae2016-10-12 06:02:30 -07001775 def GetIssueOwner(self):
1776 """Get owner from codereview, which may differ from this checkout."""
1777 return self._codereview_impl.GetIssueOwner()
1778
Edward Lemur707d70b2018-02-07 00:50:14 +01001779 def GetReviewers(self):
1780 return self._codereview_impl.GetReviewers()
1781
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001782 def GetMostRecentPatchset(self):
1783 return self._codereview_impl.GetMostRecentPatchset()
1784
tandriide281ae2016-10-12 06:02:30 -07001785 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001786 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001787 return self._codereview_impl.CannotTriggerTryJobReason()
1788
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001789 def GetTryJobProperties(self, patchset=None):
1790 """Returns dictionary of properties to launch try job."""
1791 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001792
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001793 def __getattr__(self, attr):
1794 # This is because lots of untested code accesses Rietveld-specific stuff
1795 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001796 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001797 # Note that child method defines __getattr__ as well, and forwards it here,
1798 # because _RietveldChangelistImpl is not cleaned up yet, and given
1799 # deprecation of Rietveld, it should probably be just removed.
1800 # Until that time, avoid infinite recursion by bypassing __getattr__
1801 # of implementation class.
1802 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001803
1804
1805class _ChangelistCodereviewBase(object):
1806 """Abstract base class encapsulating codereview specifics of a changelist."""
1807 def __init__(self, changelist):
1808 self._changelist = changelist # instance of Changelist
1809
1810 def __getattr__(self, attr):
1811 # Forward methods to changelist.
1812 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1813 # _RietveldChangelistImpl to avoid this hack?
1814 return getattr(self._changelist, attr)
1815
1816 def GetStatus(self):
1817 """Apply a rough heuristic to give a simple summary of an issue's review
1818 or CQ status, assuming adherence to a common workflow.
1819
1820 Returns None if no issue for this branch, or specific string keywords.
1821 """
1822 raise NotImplementedError()
1823
1824 def GetCodereviewServer(self):
1825 """Returns server URL without end slash, like "https://codereview.com"."""
1826 raise NotImplementedError()
1827
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001828 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001829 """Fetches and returns description from the codereview server."""
1830 raise NotImplementedError()
1831
tandrii5d48c322016-08-18 16:19:37 -07001832 @classmethod
1833 def IssueConfigKey(cls):
1834 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001835 raise NotImplementedError()
1836
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001837 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001838 def PatchsetConfigKey(cls):
1839 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001840 raise NotImplementedError()
1841
tandrii5d48c322016-08-18 16:19:37 -07001842 @classmethod
1843 def CodereviewServerConfigKey(cls):
1844 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001845 raise NotImplementedError()
1846
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001847 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001848 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001849 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001850
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001851 def GetGerritObjForPresubmit(self):
1852 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1853 return None
1854
dsansomee2d6fd92016-09-08 00:10:47 -07001855 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001856 """Update the description on codereview site."""
1857 raise NotImplementedError()
1858
Aaron Gable636b13f2017-07-14 10:42:48 -07001859 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001860 """Posts a comment to the codereview site."""
1861 raise NotImplementedError()
1862
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001863 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001864 raise NotImplementedError()
1865
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001866 def CloseIssue(self):
1867 """Closes the issue."""
1868 raise NotImplementedError()
1869
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001870 def GetMostRecentPatchset(self):
1871 """Returns the most recent patchset number from the codereview site."""
1872 raise NotImplementedError()
1873
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001874 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001875 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001876 """Fetches and applies the issue.
1877
1878 Arguments:
1879 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1880 reject: if True, reject the failed patch instead of switching to 3-way
1881 merge. Rietveld only.
1882 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1883 only.
1884 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001885 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001886 """
1887 raise NotImplementedError()
1888
1889 @staticmethod
1890 def ParseIssueURL(parsed_url):
1891 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1892 failed."""
1893 raise NotImplementedError()
1894
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001895 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001896 """Best effort check that user is authenticated with codereview server.
1897
1898 Arguments:
1899 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001900 refresh: whether to attempt to refresh credentials. Ignored if not
1901 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001902 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001903 raise NotImplementedError()
1904
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001905 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001906 """Best effort check that uploading isn't supposed to fail for predictable
1907 reasons.
1908
1909 This method should raise informative exception if uploading shouldn't
1910 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001911
1912 Arguments:
1913 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001914 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001915 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001916
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001917 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001918 """Uploads a change to codereview."""
1919 raise NotImplementedError()
1920
Ravi Mistry31e7d562018-04-02 12:53:57 -04001921 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1922 """Sets labels on the change based on the provided flags.
1923
1924 Issue must have been already uploaded and known.
1925 """
1926 raise NotImplementedError()
1927
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001928 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001929 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001930
1931 Issue must have been already uploaded and known.
1932 """
1933 raise NotImplementedError()
1934
tandriie113dfd2016-10-11 10:20:12 -07001935 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001936 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001937 raise NotImplementedError()
1938
tandriide281ae2016-10-12 06:02:30 -07001939 def GetIssueOwner(self):
1940 raise NotImplementedError()
1941
Edward Lemur707d70b2018-02-07 00:50:14 +01001942 def GetReviewers(self):
1943 raise NotImplementedError()
1944
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001945 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001946 raise NotImplementedError()
1947
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001948
1949class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001950
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001951 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001952 super(_RietveldChangelistImpl, self).__init__(changelist)
1953 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001954 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001955 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001956
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001957 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001958 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001959 self._props = None
1960 self._rpc_server = None
1961
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001962 def GetCodereviewServer(self):
1963 if not self._rietveld_server:
1964 # If we're on a branch then get the server potentially associated
1965 # with that branch.
1966 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001967 self._rietveld_server = gclient_utils.UpgradeToHttps(
1968 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001969 if not self._rietveld_server:
1970 self._rietveld_server = settings.GetDefaultServerUrl()
1971 return self._rietveld_server
1972
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001973 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001974 """Best effort check that user is authenticated with Rietveld server."""
1975 if self._auth_config.use_oauth2:
1976 authenticator = auth.get_authenticator_for_host(
1977 self.GetCodereviewServer(), self._auth_config)
1978 if not authenticator.has_cached_credentials():
1979 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001980 if refresh:
1981 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001982
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001983 def EnsureCanUploadPatchset(self, force):
1984 # No checks for Rietveld because we are deprecating Rietveld.
1985 pass
1986
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001987 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001988 issue = self.GetIssue()
1989 assert issue
1990 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001991 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001992 except urllib2.HTTPError as e:
1993 if e.code == 404:
1994 DieWithError(
1995 ('\nWhile fetching the description for issue %d, received a '
1996 '404 (not found)\n'
1997 'error. It is likely that you deleted this '
1998 'issue on the server. If this is the\n'
1999 'case, please run\n\n'
2000 ' git cl issue 0\n\n'
2001 'to clear the association with the deleted issue. Then run '
2002 'this command again.') % issue)
2003 else:
2004 DieWithError(
2005 '\nFailed to fetch issue description. HTTP error %d' % e.code)
2006 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07002007 print('Warning: Failed to retrieve CL description due to network '
2008 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002009 return ''
2010
2011 def GetMostRecentPatchset(self):
2012 return self.GetIssueProperties()['patchsets'][-1]
2013
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002014 def GetIssueProperties(self):
2015 if self._props is None:
2016 issue = self.GetIssue()
2017 if not issue:
2018 self._props = {}
2019 else:
2020 self._props = self.RpcServer().get_issue_properties(issue, True)
2021 return self._props
2022
tandriie113dfd2016-10-11 10:20:12 -07002023 def CannotTriggerTryJobReason(self):
2024 props = self.GetIssueProperties()
2025 if not props:
2026 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
2027 if props.get('closed'):
2028 return 'CL %s is closed' % self.GetIssue()
2029 if props.get('private'):
2030 return 'CL %s is private' % self.GetIssue()
2031 return None
2032
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002033 def GetTryJobProperties(self, patchset=None):
2034 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002035 project = (self.GetIssueProperties() or {}).get('project')
2036 return {
2037 'issue': self.GetIssue(),
2038 'patch_project': project,
2039 'patch_storage': 'rietveld',
2040 'patchset': patchset or self.GetPatchset(),
2041 'rietveld': self.GetCodereviewServer(),
2042 }
2043
tandriide281ae2016-10-12 06:02:30 -07002044 def GetIssueOwner(self):
2045 return (self.GetIssueProperties() or {}).get('owner_email')
2046
Edward Lemur707d70b2018-02-07 00:50:14 +01002047 def GetReviewers(self):
2048 return (self.GetIssueProperties() or {}).get('reviewers')
2049
Aaron Gable636b13f2017-07-14 10:42:48 -07002050 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002051 return self.RpcServer().add_comment(self.GetIssue(), message)
2052
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002053 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002054 summary = []
2055 for message in self.GetIssueProperties().get('messages', []):
2056 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2057 summary.append(_CommentSummary(
2058 date=date,
2059 disapproval=bool(message['disapproval']),
2060 approval=bool(message['approval']),
2061 sender=message['sender'],
2062 message=message['text'],
2063 ))
2064 return summary
2065
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002066 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002067 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002068 or CQ status, assuming adherence to a common workflow.
2069
2070 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002071 * 'error' - error from review tool (including deleted issues)
2072 * 'unsent' - not sent for review
2073 * 'waiting' - waiting for review
2074 * 'reply' - waiting for owner to reply to review
2075 * 'not lgtm' - Code-Review label has been set negatively
2076 * 'lgtm' - LGTM from at least one approved reviewer
2077 * 'commit' - in the commit queue
2078 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002079 """
2080 if not self.GetIssue():
2081 return None
2082
2083 try:
2084 props = self.GetIssueProperties()
2085 except urllib2.HTTPError:
2086 return 'error'
2087
2088 if props.get('closed'):
2089 # Issue is closed.
2090 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002091 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002092 # Issue is in the commit queue.
2093 return 'commit'
2094
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002095 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002096 if not messages:
2097 # No message was sent.
2098 return 'unsent'
2099
2100 if get_approving_reviewers(props):
2101 return 'lgtm'
2102 elif get_approving_reviewers(props, disapproval=True):
2103 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002104
tandrii9d2c7a32016-06-22 03:42:45 -07002105 # Skip CQ messages that don't require owner's action.
2106 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2107 if 'Dry run:' in messages[-1]['text']:
2108 messages.pop()
2109 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2110 # This message always follows prior messages from CQ,
2111 # so skip this too.
2112 messages.pop()
2113 else:
2114 # This is probably a CQ messages warranting user attention.
2115 break
2116
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002117 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002118 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002119 return 'reply'
2120 return 'waiting'
2121
dsansomee2d6fd92016-09-08 00:10:47 -07002122 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002123 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002124
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002125 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002126 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002128 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002129 return self.SetFlags({flag: value})
2130
2131 def SetFlags(self, flags):
2132 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002133 """
phajdan.jr68598232016-08-10 03:28:28 -07002134 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002135 try:
tandrii4b233bd2016-07-06 03:50:29 -07002136 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002137 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002138 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002139 if e.code == 404:
2140 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2141 if e.code == 403:
2142 DieWithError(
2143 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002144 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002145 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002146
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002147 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002148 """Returns an upload.RpcServer() to access this review's rietveld instance.
2149 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002150 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002151 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002152 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002153 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002154 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002155
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002156 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002157 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002158 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002159
tandrii5d48c322016-08-18 16:19:37 -07002160 @classmethod
2161 def PatchsetConfigKey(cls):
2162 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002163
tandrii5d48c322016-08-18 16:19:37 -07002164 @classmethod
2165 def CodereviewServerConfigKey(cls):
2166 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002167
Ravi Mistry31e7d562018-04-02 12:53:57 -04002168 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2169 raise NotImplementedError()
2170
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002171 def SetCQState(self, new_state):
2172 props = self.GetIssueProperties()
2173 if props.get('private'):
2174 DieWithError('Cannot set-commit on private issue')
2175
2176 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002177 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002178 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002179 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002180 else:
tandrii4b233bd2016-07-06 03:50:29 -07002181 assert new_state == _CQState.DRY_RUN
2182 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002183
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002184 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002185 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002186 # PatchIssue should never be called with a dirty tree. It is up to the
2187 # caller to check this, but just in case we assert here since the
2188 # consequences of the caller not checking this could be dire.
2189 assert(not git_common.is_dirty_git_tree('apply'))
2190 assert(parsed_issue_arg.valid)
2191 self._changelist.issue = parsed_issue_arg.issue
2192 if parsed_issue_arg.hostname:
2193 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2194
skobes6468b902016-10-24 08:45:10 -07002195 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2196 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2197 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002198 try:
skobes6468b902016-10-24 08:45:10 -07002199 scm_obj.apply_patch(patchset_object)
2200 except Exception as e:
2201 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002202 return 1
2203
2204 # If we had an issue, commit the current state and register the issue.
2205 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002206 self.SetIssue(self.GetIssue())
2207 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002208 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2209 'patch from issue %(i)s at patchset '
2210 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2211 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002212 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002213 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002214 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002215 return 0
2216
2217 @staticmethod
2218 def ParseIssueURL(parsed_url):
2219 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2220 return None
wychen3c1c1722016-08-04 11:46:36 -07002221 # Rietveld patch: https://domain/<number>/#ps<patchset>
2222 match = re.match(r'/(\d+)/$', parsed_url.path)
2223 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2224 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002225 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002226 issue=int(match.group(1)),
2227 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002228 hostname=parsed_url.netloc,
2229 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002230 # Typical url: https://domain/<issue_number>[/[other]]
2231 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2232 if match:
skobes6468b902016-10-24 08:45:10 -07002233 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002234 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002235 hostname=parsed_url.netloc,
2236 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002237 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2238 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2239 if match:
skobes6468b902016-10-24 08:45:10 -07002240 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002241 issue=int(match.group(1)),
2242 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002243 hostname=parsed_url.netloc,
2244 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002245 return None
2246
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002247 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002248 """Upload the patch to Rietveld."""
2249 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2250 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002251 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2252 if options.emulate_svn_auto_props:
2253 upload_args.append('--emulate_svn_auto_props')
2254
2255 change_desc = None
2256
2257 if options.email is not None:
2258 upload_args.extend(['--email', options.email])
2259
2260 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002261 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002262 upload_args.extend(['--title', options.title])
2263 if options.message:
2264 upload_args.extend(['--message', options.message])
2265 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002266 print('This branch is associated with issue %s. '
2267 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002268 else:
nodirca166002016-06-27 10:59:51 -07002269 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002270 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002271 if options.message:
2272 message = options.message
2273 else:
2274 message = CreateDescriptionFromLog(args)
2275 if options.title:
2276 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002277 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002278 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002279 change_desc.update_reviewers(options.reviewers, options.tbrs,
2280 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002281 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002282 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002283
2284 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002285 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002286 return 1
2287
2288 upload_args.extend(['--message', change_desc.description])
2289 if change_desc.get_reviewers():
2290 upload_args.append('--reviewers=%s' % ','.join(
2291 change_desc.get_reviewers()))
2292 if options.send_mail:
2293 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002294 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002295 upload_args.append('--send_mail')
2296
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00002297 # We only skip auto-CC-ing addresses from rietveld.cc when --private or
2298 # --no-autocc is explicitly specified on the command line. Should private
2299 # CL be created due to rietveld.private value, we assume that rietveld.cc
2300 # only contains addresses where private CLs are allowed to be sent.
2301 if options.private or options.no_autocc:
2302 logging.warn('rietveld.cc is ignored since private/no-autocc flag is '
2303 'specified. You need to review and add them manually if '
2304 'necessary.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002305 cc = self.GetCCListWithoutDefault()
2306 else:
2307 cc = self.GetCCList()
2308 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002309 if change_desc.get_cced():
2310 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002311 if cc:
2312 upload_args.extend(['--cc', cc])
2313
2314 if options.private or settings.GetDefaultPrivateFlag() == "True":
2315 upload_args.append('--private')
2316
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002317 # Include the upstream repo's URL in the change -- this is useful for
2318 # projects that have their source spread across multiple repos.
2319 remote_url = self.GetGitBaseUrlFromConfig()
2320 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002321 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2322 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2323 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002324 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002325 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002326 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002327 if target_ref:
2328 upload_args.extend(['--target_ref', target_ref])
2329
2330 # Look for dependent patchsets. See crbug.com/480453 for more details.
2331 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2332 upstream_branch = ShortBranchName(upstream_branch)
2333 if remote is '.':
2334 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002335 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002336 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002337 print()
2338 print('Skipping dependency patchset upload because git config '
2339 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2340 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002341 else:
2342 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002343 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002344 auth_config=auth_config)
2345 branch_cl_issue_url = branch_cl.GetIssueURL()
2346 branch_cl_issue = branch_cl.GetIssue()
2347 branch_cl_patchset = branch_cl.GetPatchset()
2348 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2349 upload_args.extend(
2350 ['--depends_on_patchset', '%s:%s' % (
2351 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002352 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002353 '\n'
2354 'The current branch (%s) is tracking a local branch (%s) with '
2355 'an associated CL.\n'
2356 'Adding %s/#ps%s as a dependency patchset.\n'
2357 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2358 branch_cl_patchset))
2359
2360 project = settings.GetProject()
2361 if project:
2362 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002363 else:
2364 print()
2365 print('WARNING: Uploading without a project specified. Please ensure '
2366 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2367 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002368
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002369 try:
2370 upload_args = ['upload'] + upload_args + args
2371 logging.info('upload.RealMain(%s)', upload_args)
2372 issue, patchset = upload.RealMain(upload_args)
2373 issue = int(issue)
2374 patchset = int(patchset)
2375 except KeyboardInterrupt:
2376 sys.exit(1)
2377 except:
2378 # If we got an exception after the user typed a description for their
2379 # change, back up the description before re-raising.
2380 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002381 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002382 raise
2383
2384 if not self.GetIssue():
2385 self.SetIssue(issue)
2386 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002387 return 0
2388
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002389
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002390class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002391 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002392 # auth_config is Rietveld thing, kept here to preserve interface only.
2393 super(_GerritChangelistImpl, self).__init__(changelist)
2394 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002395 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002396 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002397 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002398 # Map from change number (issue) to its detail cache.
2399 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002400
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002401 if codereview_host is not None:
2402 assert not codereview_host.startswith('https://'), codereview_host
2403 self._gerrit_host = codereview_host
2404 self._gerrit_server = 'https://%s' % codereview_host
2405
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002406 def _GetGerritHost(self):
2407 # Lazy load of configs.
2408 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002409 if self._gerrit_host and '.' not in self._gerrit_host:
2410 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2411 # This happens for internal stuff http://crbug.com/614312.
2412 parsed = urlparse.urlparse(self.GetRemoteUrl())
2413 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002414 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002415 ' Your current remote is: %s' % self.GetRemoteUrl())
2416 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2417 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002418 return self._gerrit_host
2419
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002420 def _GetGitHost(self):
2421 """Returns git host to be used when uploading change to Gerrit."""
2422 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2423
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002424 def GetCodereviewServer(self):
2425 if not self._gerrit_server:
2426 # If we're on a branch then get the server potentially associated
2427 # with that branch.
2428 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002429 self._gerrit_server = self._GitGetBranchConfigValue(
2430 self.CodereviewServerConfigKey())
2431 if self._gerrit_server:
2432 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002433 if not self._gerrit_server:
2434 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2435 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002436 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002437 parts[0] = parts[0] + '-review'
2438 self._gerrit_host = '.'.join(parts)
2439 self._gerrit_server = 'https://%s' % self._gerrit_host
2440 return self._gerrit_server
2441
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002442 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002443 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002444 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002445 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002446 logging.warn('can\'t detect Gerrit project.')
2447 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002448 project = urlparse.urlparse(remote_url).path.strip('/')
2449 if project.endswith('.git'):
2450 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00002451 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
2452 # 'a/' prefix, because 'a/' prefix is used to force authentication in
2453 # gitiles/git-over-https protocol. E.g.,
2454 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
2455 # as
2456 # https://chromium.googlesource.com/v8/v8
2457 if project.startswith('a/'):
2458 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002459 return project
2460
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002461 def _GerritChangeIdentifier(self):
2462 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2463
2464 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002465 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002466 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002467 project = self._GetGerritProject()
2468 if project:
2469 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2470 # Fall back on still unique, but less efficient change number.
2471 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002472
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002473 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002474 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002475 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002476
tandrii5d48c322016-08-18 16:19:37 -07002477 @classmethod
2478 def PatchsetConfigKey(cls):
2479 return 'gerritpatchset'
2480
2481 @classmethod
2482 def CodereviewServerConfigKey(cls):
2483 return 'gerritserver'
2484
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002485 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002486 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002487 if settings.GetGerritSkipEnsureAuthenticated():
2488 # For projects with unusual authentication schemes.
2489 # See http://crbug.com/603378.
2490 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002491 # Lazy-loader to identify Gerrit and Git hosts.
2492 if gerrit_util.GceAuthenticator.is_gce():
2493 return
2494 self.GetCodereviewServer()
2495 git_host = self._GetGitHost()
2496 assert self._gerrit_server and self._gerrit_host
2497 cookie_auth = gerrit_util.CookiesAuthenticator()
2498
2499 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2500 git_auth = cookie_auth.get_auth_header(git_host)
2501 if gerrit_auth and git_auth:
2502 if gerrit_auth == git_auth:
2503 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002504 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002505 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002506 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002507 ' %s\n'
2508 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002509 ' Consider running the following command:\n'
2510 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002511 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002512 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002513 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002514 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002515 cookie_auth.get_new_password_message(git_host)))
2516 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002517 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002518 return
2519 else:
2520 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002521 ([] if gerrit_auth else [self._gerrit_host]) +
2522 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002523 DieWithError('Credentials for the following hosts are required:\n'
2524 ' %s\n'
2525 'These are read from %s (or legacy %s)\n'
2526 '%s' % (
2527 '\n '.join(missing),
2528 cookie_auth.get_gitcookies_path(),
2529 cookie_auth.get_netrc_path(),
2530 cookie_auth.get_new_password_message(git_host)))
2531
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002532 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002533 if not self.GetIssue():
2534 return
2535
2536 # Warm change details cache now to avoid RPCs later, reducing latency for
2537 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002538 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002539 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002540
2541 status = self._GetChangeDetail()['status']
2542 if status in ('MERGED', 'ABANDONED'):
2543 DieWithError('Change %s has been %s, new uploads are not allowed' %
2544 (self.GetIssueURL(),
2545 'submitted' if status == 'MERGED' else 'abandoned'))
2546
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002547 if gerrit_util.GceAuthenticator.is_gce():
2548 return
2549 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2550 self._GetGerritHost())
2551 if self.GetIssueOwner() == cookies_user:
2552 return
2553 logging.debug('change %s owner is %s, cookies user is %s',
2554 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002555 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002556 # so ask what Gerrit thinks of this user.
2557 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2558 if details['email'] == self.GetIssueOwner():
2559 return
2560 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002561 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002562 'as %s.\n'
2563 'Uploading may fail due to lack of permissions.' %
2564 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2565 confirm_or_exit(action='upload')
2566
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002567 def _PostUnsetIssueProperties(self):
2568 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002569 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002570
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002571 def GetGerritObjForPresubmit(self):
2572 return presubmit_support.GerritAccessor(self._GetGerritHost())
2573
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002574 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002575 """Apply a rough heuristic to give a simple summary of an issue's review
2576 or CQ status, assuming adherence to a common workflow.
2577
2578 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002579 * 'error' - error from review tool (including deleted issues)
2580 * 'unsent' - no reviewers added
2581 * 'waiting' - waiting for review
2582 * 'reply' - waiting for uploader to reply to review
2583 * 'lgtm' - Code-Review label has been set
2584 * 'commit' - in the commit queue
2585 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002586 """
2587 if not self.GetIssue():
2588 return None
2589
2590 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002591 data = self._GetChangeDetail([
2592 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002593 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002594 return 'error'
2595
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002596 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002597 return 'closed'
2598
Aaron Gable9ab38c62017-04-06 14:36:33 -07002599 if data['labels'].get('Commit-Queue', {}).get('approved'):
2600 # The section will have an "approved" subsection if anyone has voted
2601 # the maximum value on the label.
2602 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002603
Aaron Gable9ab38c62017-04-06 14:36:33 -07002604 if data['labels'].get('Code-Review', {}).get('approved'):
2605 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002606
2607 if not data.get('reviewers', {}).get('REVIEWER', []):
2608 return 'unsent'
2609
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002610 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002611 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2612 last_message_author = messages.pop().get('author', {})
2613 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002614 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2615 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002616 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002617 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002618 if last_message_author.get('_account_id') == owner:
2619 # Most recent message was by owner.
2620 return 'waiting'
2621 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002622 # Some reply from non-owner.
2623 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002624
2625 # Somehow there are no messages even though there are reviewers.
2626 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002627
2628 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002629 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002630 patchset = data['revisions'][data['current_revision']]['_number']
2631 self.SetPatchset(patchset)
2632 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002633
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002634 def FetchDescription(self, force=False):
2635 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2636 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002637 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002638 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002639
dsansomee2d6fd92016-09-08 00:10:47 -07002640 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002641 if gerrit_util.HasPendingChangeEdit(
2642 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002643 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002644 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002645 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002646 'unpublished edit. Either publish the edit in the Gerrit web UI '
2647 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002648
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002649 gerrit_util.DeletePendingChangeEdit(
2650 self._GetGerritHost(), self._GerritChangeIdentifier())
2651 gerrit_util.SetCommitMessage(
2652 self._GetGerritHost(), self._GerritChangeIdentifier(),
2653 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002654
Aaron Gable636b13f2017-07-14 10:42:48 -07002655 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002656 gerrit_util.SetReview(
2657 self._GetGerritHost(), self._GerritChangeIdentifier(),
2658 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002659
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002660 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002661 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002662 messages = self._GetChangeDetail(
2663 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2664 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002665 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002666
2667 # Build dictionary of file comments for easy access and sorting later.
2668 # {author+date: {path: {patchset: {line: url+message}}}}
2669 comments = collections.defaultdict(
2670 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2671 for path, line_comments in file_comments.iteritems():
2672 for comment in line_comments:
2673 if comment.get('tag', '').startswith('autogenerated'):
2674 continue
2675 key = (comment['author']['email'], comment['updated'])
2676 if comment.get('side', 'REVISION') == 'PARENT':
2677 patchset = 'Base'
2678 else:
2679 patchset = 'PS%d' % comment['patch_set']
2680 line = comment.get('line', 0)
2681 url = ('https://%s/c/%s/%s/%s#%s%s' %
2682 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2683 'b' if comment.get('side') == 'PARENT' else '',
2684 str(line) if line else ''))
2685 comments[key][path][patchset][line] = (url, comment['message'])
2686
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002687 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002688 for msg in messages:
2689 # Don't bother showing autogenerated messages.
2690 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2691 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002692 # Gerrit spits out nanoseconds.
2693 assert len(msg['date'].split('.')[-1]) == 9
2694 date = datetime.datetime.strptime(msg['date'][:-3],
2695 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002696 message = msg['message']
2697 key = (msg['author']['email'], msg['date'])
2698 if key in comments:
2699 message += '\n'
2700 for path, patchsets in sorted(comments.get(key, {}).items()):
2701 if readable:
2702 message += '\n%s' % path
2703 for patchset, lines in sorted(patchsets.items()):
2704 for line, (url, content) in sorted(lines.items()):
2705 if line:
2706 line_str = 'Line %d' % line
2707 path_str = '%s:%d:' % (path, line)
2708 else:
2709 line_str = 'File comment'
2710 path_str = '%s:0:' % path
2711 if readable:
2712 message += '\n %s, %s: %s' % (patchset, line_str, url)
2713 message += '\n %s\n' % content
2714 else:
2715 message += '\n%s ' % path_str
2716 message += '\n%s\n' % content
2717
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002718 summary.append(_CommentSummary(
2719 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002720 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002721 sender=msg['author']['email'],
2722 # These could be inferred from the text messages and correlated with
2723 # Code-Review label maximum, however this is not reliable.
2724 # Leaving as is until the need arises.
2725 approval=False,
2726 disapproval=False,
2727 ))
2728 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002729
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002730 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002731 gerrit_util.AbandonChange(
2732 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002733
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002734 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002735 gerrit_util.SubmitChange(
2736 self._GetGerritHost(), self._GerritChangeIdentifier(),
2737 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002738
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002739 def _GetChangeDetail(self, options=None, no_cache=False):
2740 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002741
2742 If fresh data is needed, set no_cache=True which will clear cache and
2743 thus new data will be fetched from Gerrit.
2744 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002745 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002746 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002747
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002748 # Optimization to avoid multiple RPCs:
2749 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2750 'CURRENT_COMMIT' not in options):
2751 options.append('CURRENT_COMMIT')
2752
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002753 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002754 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002755 options = [o.upper() for o in options]
2756
2757 # Check in cache first unless no_cache is True.
2758 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002759 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002760 else:
2761 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002762 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002763 # Assumption: data fetched before with extra options is suitable
2764 # for return for a smaller set of options.
2765 # For example, if we cached data for
2766 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2767 # and request is for options=[CURRENT_REVISION],
2768 # THEN we can return prior cached data.
2769 if options_set.issubset(cached_options_set):
2770 return data
2771
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002772 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002773 data = gerrit_util.GetChangeDetail(
2774 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002775 except gerrit_util.GerritError as e:
2776 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002777 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002778 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002779
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002780 self._detail_cache.setdefault(cache_key, []).append(
2781 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002782 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002783
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002784 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002785 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002786 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002787 data = gerrit_util.GetChangeCommit(
2788 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002789 except gerrit_util.GerritError as e:
2790 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002791 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002792 raise
agable32978d92016-11-01 12:55:02 -07002793 return data
2794
Olivier Robin75ee7252018-04-13 10:02:56 +02002795 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002796 if git_common.is_dirty_git_tree('land'):
2797 return 1
tandriid60367b2016-06-22 05:25:12 -07002798 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2799 if u'Commit-Queue' in detail.get('labels', {}):
2800 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002801 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2802 'which can test and land changes for you. '
2803 'Are you sure you wish to bypass it?\n',
2804 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002805
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002806 differs = True
tandriic4344b52016-08-29 06:04:54 -07002807 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002808 # Note: git diff outputs nothing if there is no diff.
2809 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002810 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002811 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002812 if detail['current_revision'] == last_upload:
2813 differs = False
2814 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002815 print('WARNING: Local branch contents differ from latest uploaded '
2816 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002817 if differs:
2818 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002819 confirm_or_exit(
2820 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2821 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002822 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002823 elif not bypass_hooks:
2824 hook_results = self.RunHook(
2825 committing=True,
2826 may_prompt=not force,
2827 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002828 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2829 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002830 if not hook_results.should_continue():
2831 return 1
2832
2833 self.SubmitIssue(wait_for_merge=True)
2834 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002835 links = self._GetChangeCommit().get('web_links', [])
2836 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002837 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002838 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002839 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002840 return 0
2841
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002842 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002843 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002844 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002845 assert not directory
2846 assert parsed_issue_arg.valid
2847
2848 self._changelist.issue = parsed_issue_arg.issue
2849
2850 if parsed_issue_arg.hostname:
2851 self._gerrit_host = parsed_issue_arg.hostname
2852 self._gerrit_server = 'https://%s' % self._gerrit_host
2853
tandriic2405f52016-10-10 08:13:15 -07002854 try:
2855 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002856 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002857 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002858
2859 if not parsed_issue_arg.patchset:
2860 # Use current revision by default.
2861 revision_info = detail['revisions'][detail['current_revision']]
2862 patchset = int(revision_info['_number'])
2863 else:
2864 patchset = parsed_issue_arg.patchset
2865 for revision_info in detail['revisions'].itervalues():
2866 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2867 break
2868 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002869 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002870 (parsed_issue_arg.patchset, self.GetIssue()))
2871
Aaron Gable697a91b2018-01-19 15:20:15 -08002872 remote_url = self._changelist.GetRemoteUrl()
2873 if remote_url.endswith('.git'):
2874 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002875 remote_url = remote_url.rstrip('/')
2876
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002877 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002878 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002879
2880 if remote_url != fetch_info['url']:
2881 DieWithError('Trying to patch a change from %s but this repo appears '
2882 'to be %s.' % (fetch_info['url'], remote_url))
2883
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002884 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002885
Aaron Gable62619a32017-06-16 08:22:09 -07002886 if force:
2887 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2888 print('Checked out commit for change %i patchset %i locally' %
2889 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002890 elif nocommit:
2891 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2892 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002893 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002894 RunGit(['cherry-pick', 'FETCH_HEAD'])
2895 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002896 (parsed_issue_arg.issue, patchset))
2897 print('Note: this created a local commit which does not have '
2898 'the same hash as the one uploaded for review. This will make '
2899 'uploading changes based on top of this branch difficult.\n'
2900 'If you want to do that, use "git cl patch --force" instead.')
2901
Stefan Zagerd08043c2017-10-12 12:07:02 -07002902 if self.GetBranch():
2903 self.SetIssue(parsed_issue_arg.issue)
2904 self.SetPatchset(patchset)
2905 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2906 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2907 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2908 else:
2909 print('WARNING: You are in detached HEAD state.\n'
2910 'The patch has been applied to your checkout, but you will not be '
2911 'able to upload a new patch set to the gerrit issue.\n'
2912 'Try using the \'-b\' option if you would like to work on a '
2913 'branch and/or upload a new patch set.')
2914
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002915 return 0
2916
2917 @staticmethod
2918 def ParseIssueURL(parsed_url):
2919 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2920 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002921 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2922 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002923 # Short urls like https://domain/<issue_number> can be used, but don't allow
2924 # specifying the patchset (you'd 404), but we allow that here.
2925 if parsed_url.path == '/':
2926 part = parsed_url.fragment
2927 else:
2928 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002929 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002930 if match:
2931 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002932 issue=int(match.group(3)),
2933 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002934 hostname=parsed_url.netloc,
2935 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002936 return None
2937
tandrii16e0b4e2016-06-07 10:34:28 -07002938 def _GerritCommitMsgHookCheck(self, offer_removal):
2939 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2940 if not os.path.exists(hook):
2941 return
2942 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2943 # custom developer made one.
2944 data = gclient_utils.FileRead(hook)
2945 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2946 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002947 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002948 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002949 'and may interfere with it in subtle ways.\n'
2950 'We recommend you remove the commit-msg hook.')
2951 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002952 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002953 gclient_utils.rm_file_or_tree(hook)
2954 print('Gerrit commit-msg hook removed.')
2955 else:
2956 print('OK, will keep Gerrit commit-msg hook in place.')
2957
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002958 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002959 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002960 if options.squash and options.no_squash:
2961 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002962
2963 if not options.squash and not options.no_squash:
2964 # Load default for user, repo, squash=true, in this order.
2965 options.squash = settings.GetSquashGerritUploads()
2966 elif options.no_squash:
2967 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002968
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002969 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002970 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002971
Aaron Gableb56ad332017-01-06 15:24:31 -08002972 # This may be None; default fallback value is determined in logic below.
2973 title = options.title
2974
Dominic Battre7d1c4842017-10-27 09:17:28 +02002975 # Extract bug number from branch name.
2976 bug = options.bug
2977 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2978 if not bug and match:
2979 bug = match.group(1)
2980
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002981 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002982 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002983 if self.GetIssue():
2984 # Try to get the message from a previous upload.
2985 message = self.GetDescription()
2986 if not message:
2987 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002988 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002989 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002990 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002991 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002992 # When uploading a subsequent patchset, -m|--message is taken
2993 # as the patchset title if --title was not provided.
2994 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002995 else:
2996 default_title = RunGit(
2997 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002998 if options.force:
2999 title = default_title
3000 else:
3001 title = ask_for_data(
3002 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003003 change_id = self._GetChangeDetail()['change_id']
3004 while True:
3005 footer_change_ids = git_footers.get_footer_change_id(message)
3006 if footer_change_ids == [change_id]:
3007 break
3008 if not footer_change_ids:
3009 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003010 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003011 continue
3012 # There is already a valid footer but with different or several ids.
3013 # Doing this automatically is non-trivial as we don't want to lose
3014 # existing other footers, yet we want to append just 1 desired
3015 # Change-Id. Thus, just create a new footer, but let user verify the
3016 # new description.
3017 message = '%s\n\nChange-Id: %s' % (message, change_id)
3018 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08003019 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003020 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08003021 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003022 'Please, check the proposed correction to the description, '
3023 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
3024 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
3025 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003026 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003027 if not options.force:
3028 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02003029 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003030 message = change_desc.description
3031 if not message:
3032 DieWithError("Description is empty. Aborting...")
3033 # Continue the while loop.
3034 # Sanity check of this code - we should end up with proper message
3035 # footer.
3036 assert [change_id] == git_footers.get_footer_change_id(message)
3037 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08003038 else: # if not self.GetIssue()
3039 if options.message:
3040 message = options.message
3041 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003042 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08003043 if options.title:
3044 message = options.title + '\n\n' + message
3045 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003046
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003047 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02003048 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08003049 # On first upload, patchset title is always this string, while
3050 # --title flag gets converted to first line of message.
3051 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003052 if not change_desc.description:
3053 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003054 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003055 if len(change_ids) > 1:
3056 DieWithError('too many Change-Id footers, at most 1 allowed.')
3057 if not change_ids:
3058 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003059 change_desc.set_description(git_footers.add_footer_change_id(
3060 change_desc.description,
3061 GenerateGerritChangeId(change_desc.description)))
3062 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003063 assert len(change_ids) == 1
3064 change_id = change_ids[0]
3065
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003066 if options.reviewers or options.tbrs or options.add_owners_to:
3067 change_desc.update_reviewers(options.reviewers, options.tbrs,
3068 options.add_owners_to, change)
3069
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003070 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003071 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
3072 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003073 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07003074 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
3075 desc_tempfile.write(change_desc.description)
3076 desc_tempfile.close()
3077 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
3078 '-F', desc_tempfile.name]).strip()
3079 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003080 else:
3081 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003082 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003083 if not change_desc.description:
3084 DieWithError("Description is empty. Aborting...")
3085
3086 if not git_footers.get_footer_change_id(change_desc.description):
3087 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003088 change_desc.set_description(
3089 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003090 if options.reviewers or options.tbrs or options.add_owners_to:
3091 change_desc.update_reviewers(options.reviewers, options.tbrs,
3092 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003093 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003094 # For no-squash mode, we assume the remote called "origin" is the one we
3095 # want. It is not worthwhile to support different workflows for
3096 # no-squash mode.
3097 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003098 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3099
3100 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00003101 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003102 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3103 ref_to_push)]).splitlines()
3104 if len(commits) > 1:
3105 print('WARNING: This will upload %d commits. Run the following command '
3106 'to see which commits will be uploaded: ' % len(commits))
3107 print('git log %s..%s' % (parent, ref_to_push))
3108 print('You can also use `git squash-branch` to squash these into a '
3109 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003110 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003111
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003112 if options.reviewers or options.tbrs or options.add_owners_to:
3113 change_desc.update_reviewers(options.reviewers, options.tbrs,
3114 options.add_owners_to, change)
3115
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003116 # Extra options that can be specified at push time. Doc:
3117 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003118 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003119
Aaron Gable844cf292017-06-28 11:32:59 -07003120 # By default, new changes are started in WIP mode, and subsequent patchsets
3121 # don't send email. At any time, passing --send-mail will mark the change
3122 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003123 if options.send_mail:
3124 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003125 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04003126 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003127 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003128 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003129 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003130
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003131 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003132 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003133
Aaron Gable9b713dd2016-12-14 16:04:21 -08003134 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003135 # Punctuation and whitespace in |title| must be percent-encoded.
3136 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003137
agablec6787972016-09-09 16:13:34 -07003138 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003139 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003140
rmistry9eadede2016-09-19 11:22:43 -07003141 if options.topic:
3142 # Documentation on Gerrit topics is here:
3143 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003144 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003145
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003146 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003147 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003148 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003149 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003150 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3151
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003152 refspec_suffix = ''
3153 if refspec_opts:
3154 refspec_suffix = '%' + ','.join(refspec_opts)
3155 assert ' ' not in refspec_suffix, (
3156 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3157 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3158
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003159 try:
3160 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00003161 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003162 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003163 # Flush after every line: useful for seeing progress when running as
3164 # recipe.
3165 filter_fn=lambda _: sys.stdout.flush())
3166 except subprocess2.CalledProcessError:
3167 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003168 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003169 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003170 'credential problems:\n'
3171 ' git cl creds-check\n',
3172 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003173
3174 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003175 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003176 change_numbers = [m.group(1)
3177 for m in map(regex.match, push_stdout.splitlines())
3178 if m]
3179 if len(change_numbers) != 1:
3180 DieWithError(
3181 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003182 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003183 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003184 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003185
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003186 reviewers = sorted(change_desc.get_reviewers())
3187
tandrii88189772016-09-29 04:29:57 -07003188 # Add cc's from the CC_LIST and --cc flag (if any).
Sergiy Byelozyorovaaf2cc02018-09-24 18:02:28 +00003189 if not options.private and not options.no_autocc:
Aaron Gabled1052492017-05-15 15:05:34 -07003190 cc = self.GetCCList().split(',')
3191 else:
3192 cc = []
tandrii88189772016-09-29 04:29:57 -07003193 if options.cc:
3194 cc.extend(options.cc)
3195 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003196 if change_desc.get_cced():
3197 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003198
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003199 if self.GetIssue():
3200 # GetIssue() is not set in case of non-squash uploads according to tests.
3201 # TODO(agable): non-squash uploads in git cl should be removed.
3202 gerrit_util.AddReviewers(
3203 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003204 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003205 reviewers, cc,
3206 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003207
Aaron Gablefd238082017-06-07 13:42:34 -07003208 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003209 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3210 score = 1
3211 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3212 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3213 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003214 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003215 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003216 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003217 msg='Self-approving for TBR',
3218 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003219
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003220 return 0
3221
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003222 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3223 change_desc):
3224 """Computes parent of the generated commit to be uploaded to Gerrit.
3225
3226 Returns revision or a ref name.
3227 """
3228 if custom_cl_base:
3229 # Try to avoid creating additional unintended CLs when uploading, unless
3230 # user wants to take this risk.
3231 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3232 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3233 local_ref_of_target_remote])
3234 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003235 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003236 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3237 'If you proceed with upload, more than 1 CL may be created by '
3238 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3239 'If you are certain that specified base `%s` has already been '
3240 'uploaded to Gerrit as another CL, you may proceed.\n' %
3241 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3242 if not force:
3243 confirm_or_exit(
3244 'Do you take responsibility for cleaning up potential mess '
3245 'resulting from proceeding with upload?',
3246 action='upload')
3247 return custom_cl_base
3248
Aaron Gablef97e33d2017-03-30 15:44:27 -07003249 if remote != '.':
3250 return self.GetCommonAncestorWithUpstream()
3251
3252 # If our upstream branch is local, we base our squashed commit on its
3253 # squashed version.
3254 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3255
Aaron Gablef97e33d2017-03-30 15:44:27 -07003256 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003257 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003258
3259 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003260 # TODO(tandrii): consider checking parent change in Gerrit and using its
3261 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3262 # the tree hash of the parent branch. The upside is less likely bogus
3263 # requests to reupload parent change just because it's uploadhash is
3264 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003265 parent = RunGit(['config',
3266 'branch.%s.gerritsquashhash' % upstream_branch_name],
3267 error_ok=True).strip()
3268 # Verify that the upstream branch has been uploaded too, otherwise
3269 # Gerrit will create additional CLs when uploading.
3270 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3271 RunGitSilent(['rev-parse', parent + ':'])):
3272 DieWithError(
3273 '\nUpload upstream branch %s first.\n'
3274 'It is likely that this branch has been rebased since its last '
3275 'upload, so you just need to upload it again.\n'
3276 '(If you uploaded it with --no-squash, then branch dependencies '
3277 'are not supported, and you should reupload with --squash.)'
3278 % upstream_branch_name,
3279 change_desc)
3280 return parent
3281
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003282 def _AddChangeIdToCommitMessage(self, options, args):
3283 """Re-commits using the current message, assumes the commit hook is in
3284 place.
3285 """
3286 log_desc = options.message or CreateDescriptionFromLog(args)
3287 git_command = ['commit', '--amend', '-m', log_desc]
3288 RunGit(git_command)
3289 new_log_desc = CreateDescriptionFromLog(args)
3290 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003291 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003292 return new_log_desc
3293 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003294 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003295
Ravi Mistry31e7d562018-04-02 12:53:57 -04003296 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3297 """Sets labels on the change based on the provided flags."""
3298 labels = {}
3299 notify = None;
3300 if enable_auto_submit:
3301 labels['Auto-Submit'] = 1
3302 if use_commit_queue:
3303 labels['Commit-Queue'] = 2
3304 elif cq_dry_run:
3305 labels['Commit-Queue'] = 1
3306 notify = False
3307 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003308 gerrit_util.SetReview(
3309 self._GetGerritHost(),
3310 self._GerritChangeIdentifier(),
3311 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003312
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003313 def SetCQState(self, new_state):
3314 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003315 vote_map = {
3316 _CQState.NONE: 0,
3317 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003318 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003319 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003320 labels = {'Commit-Queue': vote_map[new_state]}
3321 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003322 gerrit_util.SetReview(
3323 self._GetGerritHost(), self._GerritChangeIdentifier(),
3324 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003325
tandriie113dfd2016-10-11 10:20:12 -07003326 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003327 try:
3328 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003329 except GerritChangeNotExists:
3330 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003331
3332 if data['status'] in ('ABANDONED', 'MERGED'):
3333 return 'CL %s is closed' % self.GetIssue()
3334
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003335 def GetTryJobProperties(self, patchset=None):
3336 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003337 data = self._GetChangeDetail(['ALL_REVISIONS'])
3338 patchset = int(patchset or self.GetPatchset())
3339 assert patchset
3340 revision_data = None # Pylint wants it to be defined.
3341 for revision_data in data['revisions'].itervalues():
3342 if int(revision_data['_number']) == patchset:
3343 break
3344 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003345 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003346 (patchset, self.GetIssue()))
3347 return {
3348 'patch_issue': self.GetIssue(),
3349 'patch_set': patchset or self.GetPatchset(),
3350 'patch_project': data['project'],
3351 'patch_storage': 'gerrit',
3352 'patch_ref': revision_data['fetch']['http']['ref'],
3353 'patch_repository_url': revision_data['fetch']['http']['url'],
3354 'patch_gerrit_url': self.GetCodereviewServer(),
3355 }
tandriie113dfd2016-10-11 10:20:12 -07003356
tandriide281ae2016-10-12 06:02:30 -07003357 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003358 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003359
Edward Lemur707d70b2018-02-07 00:50:14 +01003360 def GetReviewers(self):
3361 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3362 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3363
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003364
3365_CODEREVIEW_IMPLEMENTATIONS = {
3366 'rietveld': _RietveldChangelistImpl,
3367 'gerrit': _GerritChangelistImpl,
3368}
3369
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003370
iannuccie53c9352016-08-17 14:40:40 -07003371def _add_codereview_issue_select_options(parser, extra=""):
3372 _add_codereview_select_options(parser)
3373
3374 text = ('Operate on this issue number instead of the current branch\'s '
3375 'implicit issue.')
3376 if extra:
3377 text += ' '+extra
3378 parser.add_option('-i', '--issue', type=int, help=text)
3379
3380
3381def _process_codereview_issue_select_options(parser, options):
3382 _process_codereview_select_options(parser, options)
3383 if options.issue is not None and not options.forced_codereview:
3384 parser.error('--issue must be specified with either --rietveld or --gerrit')
3385
3386
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003387def _add_codereview_select_options(parser):
3388 """Appends --gerrit and --rietveld options to force specific codereview."""
3389 parser.codereview_group = optparse.OptionGroup(
3390 parser, 'EXPERIMENTAL! Codereview override options')
3391 parser.add_option_group(parser.codereview_group)
3392 parser.codereview_group.add_option(
3393 '--gerrit', action='store_true',
3394 help='Force the use of Gerrit for codereview')
3395 parser.codereview_group.add_option(
3396 '--rietveld', action='store_true',
3397 help='Force the use of Rietveld for codereview')
3398
3399
3400def _process_codereview_select_options(parser, options):
3401 if options.gerrit and options.rietveld:
3402 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3403 options.forced_codereview = None
3404 if options.gerrit:
3405 options.forced_codereview = 'gerrit'
3406 elif options.rietveld:
3407 options.forced_codereview = 'rietveld'
3408
3409
tandriif9aefb72016-07-01 09:06:51 -07003410def _get_bug_line_values(default_project, bugs):
3411 """Given default_project and comma separated list of bugs, yields bug line
3412 values.
3413
3414 Each bug can be either:
3415 * a number, which is combined with default_project
3416 * string, which is left as is.
3417
3418 This function may produce more than one line, because bugdroid expects one
3419 project per line.
3420
3421 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3422 ['v8:123', 'chromium:789']
3423 """
3424 default_bugs = []
3425 others = []
3426 for bug in bugs.split(','):
3427 bug = bug.strip()
3428 if bug:
3429 try:
3430 default_bugs.append(int(bug))
3431 except ValueError:
3432 others.append(bug)
3433
3434 if default_bugs:
3435 default_bugs = ','.join(map(str, default_bugs))
3436 if default_project:
3437 yield '%s:%s' % (default_project, default_bugs)
3438 else:
3439 yield default_bugs
3440 for other in sorted(others):
3441 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3442 yield other
3443
3444
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003445class ChangeDescription(object):
3446 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003447 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003448 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003449 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003450 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003451 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3452 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3453 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3454 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003455
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003456 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003457 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003458
agable@chromium.org42c20792013-09-12 17:34:49 +00003459 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003460 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003461 return '\n'.join(self._description_lines)
3462
3463 def set_description(self, desc):
3464 if isinstance(desc, basestring):
3465 lines = desc.splitlines()
3466 else:
3467 lines = [line.rstrip() for line in desc]
3468 while lines and not lines[0]:
3469 lines.pop(0)
3470 while lines and not lines[-1]:
3471 lines.pop(-1)
3472 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003473
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003474 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3475 """Rewrites the R=/TBR= line(s) as a single line each.
3476
3477 Args:
3478 reviewers (list(str)) - list of additional emails to use for reviewers.
3479 tbrs (list(str)) - list of additional emails to use for TBRs.
3480 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3481 the change that are missing OWNER coverage. If this is not None, you
3482 must also pass a value for `change`.
3483 change (Change) - The Change that should be used for OWNERS lookups.
3484 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003485 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003486 assert isinstance(tbrs, list), tbrs
3487
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003488 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003489 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003490
3491 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003492 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003493
3494 reviewers = set(reviewers)
3495 tbrs = set(tbrs)
3496 LOOKUP = {
3497 'TBR': tbrs,
3498 'R': reviewers,
3499 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003500
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003501 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003502 regexp = re.compile(self.R_LINE)
3503 matches = [regexp.match(line) for line in self._description_lines]
3504 new_desc = [l for i, l in enumerate(self._description_lines)
3505 if not matches[i]]
3506 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003507
agable@chromium.org42c20792013-09-12 17:34:49 +00003508 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003509
3510 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003511 for match in matches:
3512 if not match:
3513 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003514 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3515
3516 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003517 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003518 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003519 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003520 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003521 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003522 LOOKUP[add_owners_to].update(
3523 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003524
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003525 # If any folks ended up in both groups, remove them from tbrs.
3526 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003527
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003528 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3529 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003530
3531 # Put the new lines in the description where the old first R= line was.
3532 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3533 if 0 <= line_loc < len(self._description_lines):
3534 if new_tbr_line:
3535 self._description_lines.insert(line_loc, new_tbr_line)
3536 if new_r_line:
3537 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003538 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003539 if new_r_line:
3540 self.append_footer(new_r_line)
3541 if new_tbr_line:
3542 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003543
Aaron Gable3a16ed12017-03-23 10:51:55 -07003544 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003545 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003546 self.set_description([
3547 '# Enter a description of the change.',
3548 '# This will be displayed on the codereview site.',
3549 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003550 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003551 '--------------------',
3552 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003553
agable@chromium.org42c20792013-09-12 17:34:49 +00003554 regexp = re.compile(self.BUG_LINE)
3555 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003556 prefix = settings.GetBugPrefix()
3557 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003558 if git_footer:
3559 self.append_footer('Bug: %s' % ', '.join(values))
3560 else:
3561 for value in values:
3562 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003563
agable@chromium.org42c20792013-09-12 17:34:49 +00003564 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003565 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003566 if not content:
3567 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003568 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003569
Bruce Dawson2377b012018-01-11 16:46:49 -08003570 # Strip off comments and default inserted "Bug:" line.
3571 clean_lines = [line.rstrip() for line in lines if not
3572 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003573 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003574 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003575 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003576
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003577 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003578 """Adds a footer line to the description.
3579
3580 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3581 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3582 that Gerrit footers are always at the end.
3583 """
3584 parsed_footer_line = git_footers.parse_footer(line)
3585 if parsed_footer_line:
3586 # Line is a gerrit footer in the form: Footer-Key: any value.
3587 # Thus, must be appended observing Gerrit footer rules.
3588 self.set_description(
3589 git_footers.add_footer(self.description,
3590 key=parsed_footer_line[0],
3591 value=parsed_footer_line[1]))
3592 return
3593
3594 if not self._description_lines:
3595 self._description_lines.append(line)
3596 return
3597
3598 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3599 if gerrit_footers:
3600 # git_footers.split_footers ensures that there is an empty line before
3601 # actual (gerrit) footers, if any. We have to keep it that way.
3602 assert top_lines and top_lines[-1] == ''
3603 top_lines, separator = top_lines[:-1], top_lines[-1:]
3604 else:
3605 separator = [] # No need for separator if there are no gerrit_footers.
3606
3607 prev_line = top_lines[-1] if top_lines else ''
3608 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3609 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3610 top_lines.append('')
3611 top_lines.append(line)
3612 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003613
tandrii99a72f22016-08-17 14:33:24 -07003614 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003615 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003616 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003617 reviewers = [match.group(2).strip()
3618 for match in matches
3619 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003620 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003621
bradnelsond975b302016-10-23 12:20:23 -07003622 def get_cced(self):
3623 """Retrieves the list of reviewers."""
3624 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3625 cced = [match.group(2).strip() for match in matches if match]
3626 return cleanup_list(cced)
3627
Nodir Turakulov23b82142017-11-16 11:04:25 -08003628 def get_hash_tags(self):
3629 """Extracts and sanitizes a list of Gerrit hashtags."""
3630 subject = (self._description_lines or ('',))[0]
3631 subject = re.sub(
3632 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3633
3634 tags = []
3635 start = 0
3636 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3637 while True:
3638 m = bracket_exp.match(subject, start)
3639 if not m:
3640 break
3641 tags.append(self.sanitize_hash_tag(m.group(1)))
3642 start = m.end()
3643
3644 if not tags:
3645 # Try "Tag: " prefix.
3646 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3647 if m:
3648 tags.append(self.sanitize_hash_tag(m.group(1)))
3649 return tags
3650
3651 @classmethod
3652 def sanitize_hash_tag(cls, tag):
3653 """Returns a sanitized Gerrit hash tag.
3654
3655 A sanitized hashtag can be used as a git push refspec parameter value.
3656 """
3657 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3658
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003659 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3660 """Updates this commit description given the parent.
3661
3662 This is essentially what Gnumbd used to do.
3663 Consult https://goo.gl/WMmpDe for more details.
3664 """
3665 assert parent_msg # No, orphan branch creation isn't supported.
3666 assert parent_hash
3667 assert dest_ref
3668 parent_footer_map = git_footers.parse_footers(parent_msg)
3669 # This will also happily parse svn-position, which GnumbD is no longer
3670 # supporting. While we'd generate correct footers, the verifier plugin
3671 # installed in Gerrit will block such commit (ie git push below will fail).
3672 parent_position = git_footers.get_position(parent_footer_map)
3673
3674 # Cherry-picks may have last line obscuring their prior footers,
3675 # from git_footers perspective. This is also what Gnumbd did.
3676 cp_line = None
3677 if (self._description_lines and
3678 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3679 cp_line = self._description_lines.pop()
3680
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003681 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003682
3683 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3684 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003685 for i, line in enumerate(footer_lines):
3686 k, v = git_footers.parse_footer(line) or (None, None)
3687 if k and k.startswith('Cr-'):
3688 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003689
3690 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003691 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003692 if parent_position[0] == dest_ref:
3693 # Same branch as parent.
3694 number = int(parent_position[1]) + 1
3695 else:
3696 number = 1 # New branch, and extra lineage.
3697 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3698 int(parent_position[1])))
3699
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003700 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3701 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003702
3703 self._description_lines = top_lines
3704 if cp_line:
3705 self._description_lines.append(cp_line)
3706 if self._description_lines[-1] != '':
3707 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003708 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003709
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003710
Aaron Gablea1bab272017-04-11 16:38:18 -07003711def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003712 """Retrieves the reviewers that approved a CL from the issue properties with
3713 messages.
3714
3715 Note that the list may contain reviewers that are not committer, thus are not
3716 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003717
3718 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003719 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003720 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003721 return sorted(
3722 set(
3723 message['sender']
3724 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003725 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003726 )
3727 )
3728
3729
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003730def FindCodereviewSettingsFile(filename='codereview.settings'):
3731 """Finds the given file starting in the cwd and going up.
3732
3733 Only looks up to the top of the repository unless an
3734 'inherit-review-settings-ok' file exists in the root of the repository.
3735 """
3736 inherit_ok_file = 'inherit-review-settings-ok'
3737 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003738 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003739 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3740 root = '/'
3741 while True:
3742 if filename in os.listdir(cwd):
3743 if os.path.isfile(os.path.join(cwd, filename)):
3744 return open(os.path.join(cwd, filename))
3745 if cwd == root:
3746 break
3747 cwd = os.path.dirname(cwd)
3748
3749
3750def LoadCodereviewSettingsFromFile(fileobj):
3751 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003752 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003753
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003754 def SetProperty(name, setting, unset_error_ok=False):
3755 fullname = 'rietveld.' + name
3756 if setting in keyvals:
3757 RunGit(['config', fullname, keyvals[setting]])
3758 else:
3759 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3760
tandrii48df5812016-10-17 03:55:37 -07003761 if not keyvals.get('GERRIT_HOST', False):
3762 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003763 # Only server setting is required. Other settings can be absent.
3764 # In that case, we ignore errors raised during option deletion attempt.
3765 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003766 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003767 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3768 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003769 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003770 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3771 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003772 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003773 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3774 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003775
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003776 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003777 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003778
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003779 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003780 RunGit(['config', 'gerrit.squash-uploads',
3781 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003782
tandrii@chromium.org28253532016-04-14 13:46:56 +00003783 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003784 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003785 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3786
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003787 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003788 # should be of the form
3789 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3790 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003791 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3792 keyvals['ORIGIN_URL_CONFIG']])
3793
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003794
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003795def urlretrieve(source, destination):
3796 """urllib is broken for SSL connections via a proxy therefore we
3797 can't use urllib.urlretrieve()."""
3798 with open(destination, 'w') as f:
3799 f.write(urllib2.urlopen(source).read())
3800
3801
ukai@chromium.org712d6102013-11-27 00:52:58 +00003802def hasSheBang(fname):
3803 """Checks fname is a #! script."""
3804 with open(fname) as f:
3805 return f.read(2).startswith('#!')
3806
3807
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003808# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3809def DownloadHooks(*args, **kwargs):
3810 pass
3811
3812
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003813def DownloadGerritHook(force):
3814 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003815
3816 Args:
3817 force: True to update hooks. False to install hooks if not present.
3818 """
3819 if not settings.GetIsGerrit():
3820 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003821 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003822 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3823 if not os.access(dst, os.X_OK):
3824 if os.path.exists(dst):
3825 if not force:
3826 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003827 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003828 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003829 if not hasSheBang(dst):
3830 DieWithError('Not a script: %s\n'
3831 'You need to download from\n%s\n'
3832 'into .git/hooks/commit-msg and '
3833 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003834 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3835 except Exception:
3836 if os.path.exists(dst):
3837 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003838 DieWithError('\nFailed to download hooks.\n'
3839 'You need to download from\n%s\n'
3840 'into .git/hooks/commit-msg and '
3841 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003842
3843
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003844def GetRietveldCodereviewSettingsInteractively():
3845 """Prompt the user for settings."""
3846 server = settings.GetDefaultServerUrl(error_ok=True)
3847 prompt = 'Rietveld server (host[:port])'
3848 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3849 newserver = ask_for_data(prompt + ':')
3850 if not server and not newserver:
3851 newserver = DEFAULT_SERVER
3852 if newserver:
3853 newserver = gclient_utils.UpgradeToHttps(newserver)
3854 if newserver != server:
3855 RunGit(['config', 'rietveld.server', newserver])
3856
3857 def SetProperty(initial, caption, name, is_url):
3858 prompt = caption
3859 if initial:
3860 prompt += ' ("x" to clear) [%s]' % initial
3861 new_val = ask_for_data(prompt + ':')
3862 if new_val == 'x':
3863 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3864 elif new_val:
3865 if is_url:
3866 new_val = gclient_utils.UpgradeToHttps(new_val)
3867 if new_val != initial:
3868 RunGit(['config', 'rietveld.' + name, new_val])
3869
3870 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3871 SetProperty(settings.GetDefaultPrivateFlag(),
3872 'Private flag (rietveld only)', 'private', False)
3873 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3874 'tree-status-url', False)
3875 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3876 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3877 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3878 'run-post-upload-hook', False)
3879
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003880
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003881class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003882 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003883
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003884 _GOOGLESOURCE = 'googlesource.com'
3885
3886 def __init__(self):
3887 # Cached list of [host, identity, source], where source is either
3888 # .gitcookies or .netrc.
3889 self._all_hosts = None
3890
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003891 def ensure_configured_gitcookies(self):
3892 """Runs checks and suggests fixes to make git use .gitcookies from default
3893 path."""
3894 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3895 configured_path = RunGitSilent(
3896 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003897 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003898 if configured_path:
3899 self._ensure_default_gitcookies_path(configured_path, default)
3900 else:
3901 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003902
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003903 @staticmethod
3904 def _ensure_default_gitcookies_path(configured_path, default_path):
3905 assert configured_path
3906 if configured_path == default_path:
3907 print('git is already configured to use your .gitcookies from %s' %
3908 configured_path)
3909 return
3910
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003911 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003912 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3913 (configured_path, default_path))
3914
3915 if not os.path.exists(configured_path):
3916 print('However, your configured .gitcookies file is missing.')
3917 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3918 action='reconfigure')
3919 RunGit(['config', '--global', 'http.cookiefile', default_path])
3920 return
3921
3922 if os.path.exists(default_path):
3923 print('WARNING: default .gitcookies file already exists %s' %
3924 default_path)
3925 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3926 default_path)
3927
3928 confirm_or_exit('Move existing .gitcookies to default location?',
3929 action='move')
3930 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003931 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003932 print('Moved and reconfigured git to use .gitcookies from %s' %
3933 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003934
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003935 @staticmethod
3936 def _configure_gitcookies_path(default_path):
3937 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3938 if os.path.exists(netrc_path):
3939 print('You seem to be using outdated .netrc for git credentials: %s' %
3940 netrc_path)
3941 print('This tool will guide you through setting up recommended '
3942 '.gitcookies store for git credentials.\n'
3943 '\n'
3944 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3945 ' git config --global --unset http.cookiefile\n'
3946 ' mv %s %s.backup\n\n' % (default_path, default_path))
3947 confirm_or_exit(action='setup .gitcookies')
3948 RunGit(['config', '--global', 'http.cookiefile', default_path])
3949 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003950
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003951 def get_hosts_with_creds(self, include_netrc=False):
3952 if self._all_hosts is None:
3953 a = gerrit_util.CookiesAuthenticator()
3954 self._all_hosts = [
3955 (h, u, s)
3956 for h, u, s in itertools.chain(
3957 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3958 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3959 )
3960 if h.endswith(self._GOOGLESOURCE)
3961 ]
3962
3963 if include_netrc:
3964 return self._all_hosts
3965 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3966
3967 def print_current_creds(self, include_netrc=False):
3968 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3969 if not hosts:
3970 print('No Git/Gerrit credentials found')
3971 return
3972 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3973 header = [('Host', 'User', 'Which file'),
3974 ['=' * l for l in lengths]]
3975 for row in (header + hosts):
3976 print('\t'.join((('%%+%ds' % l) % s)
3977 for l, s in zip(lengths, row)))
3978
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003979 @staticmethod
3980 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003981 """Parses identity "git-<username>.domain" into <username> and domain."""
3982 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003983 # distinguishable from sub-domains. But we do know typical domains:
3984 if identity.endswith('.chromium.org'):
3985 domain = 'chromium.org'
3986 username = identity[:-len('.chromium.org')]
3987 else:
3988 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003989 if username.startswith('git-'):
3990 username = username[len('git-'):]
3991 return username, domain
3992
3993 def _get_usernames_of_domain(self, domain):
3994 """Returns list of usernames referenced by .gitcookies in a given domain."""
3995 identities_by_domain = {}
3996 for _, identity, _ in self.get_hosts_with_creds():
3997 username, domain = self._parse_identity(identity)
3998 identities_by_domain.setdefault(domain, []).append(username)
3999 return identities_by_domain.get(domain)
4000
4001 def _canonical_git_googlesource_host(self, host):
4002 """Normalizes Gerrit hosts (with '-review') to Git host."""
4003 assert host.endswith(self._GOOGLESOURCE)
4004 # Prefix doesn't include '.' at the end.
4005 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
4006 if prefix.endswith('-review'):
4007 prefix = prefix[:-len('-review')]
4008 return prefix + '.' + self._GOOGLESOURCE
4009
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004010 def _canonical_gerrit_googlesource_host(self, host):
4011 git_host = self._canonical_git_googlesource_host(host)
4012 prefix = git_host.split('.', 1)[0]
4013 return prefix + '-review.' + self._GOOGLESOURCE
4014
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004015 def _get_counterpart_host(self, host):
4016 assert host.endswith(self._GOOGLESOURCE)
4017 git = self._canonical_git_googlesource_host(host)
4018 gerrit = self._canonical_gerrit_googlesource_host(git)
4019 return git if gerrit == host else gerrit
4020
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004021 def has_generic_host(self):
4022 """Returns whether generic .googlesource.com has been configured.
4023
4024 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
4025 """
4026 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
4027 if host == '.' + self._GOOGLESOURCE:
4028 return True
4029 return False
4030
4031 def _get_git_gerrit_identity_pairs(self):
4032 """Returns map from canonic host to pair of identities (Git, Gerrit).
4033
4034 One of identities might be None, meaning not configured.
4035 """
4036 host_to_identity_pairs = {}
4037 for host, identity, _ in self.get_hosts_with_creds():
4038 canonical = self._canonical_git_googlesource_host(host)
4039 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
4040 idx = 0 if canonical == host else 1
4041 pair[idx] = identity
4042 return host_to_identity_pairs
4043
4044 def get_partially_configured_hosts(self):
4045 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004046 (host if i1 else self._canonical_gerrit_googlesource_host(host))
4047 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
4048 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004049
4050 def get_conflicting_hosts(self):
4051 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004052 host
4053 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004054 if None not in (i1, i2) and i1 != i2)
4055
4056 def get_duplicated_hosts(self):
4057 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
4058 return set(host for host, count in counters.iteritems() if count > 1)
4059
4060 _EXPECTED_HOST_IDENTITY_DOMAINS = {
4061 'chromium.googlesource.com': 'chromium.org',
4062 'chrome-internal.googlesource.com': 'google.com',
4063 }
4064
4065 def get_hosts_with_wrong_identities(self):
4066 """Finds hosts which **likely** reference wrong identities.
4067
4068 Note: skips hosts which have conflicting identities for Git and Gerrit.
4069 """
4070 hosts = set()
4071 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
4072 pair = self._get_git_gerrit_identity_pairs().get(host)
4073 if pair and pair[0] == pair[1]:
4074 _, domain = self._parse_identity(pair[0])
4075 if domain != expected:
4076 hosts.add(host)
4077 return hosts
4078
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004079 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004080 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004081 hosts = sorted(hosts)
4082 assert hosts
4083 if extra_column_func is None:
4084 extras = [''] * len(hosts)
4085 else:
4086 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004087 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
4088 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004089 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004090 lines.append(tmpl % he)
4091 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004092
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004093 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004094 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004095 yield ('.googlesource.com wildcard record detected',
4096 ['Chrome Infrastructure team recommends to list full host names '
4097 'explicitly.'],
4098 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004099
4100 dups = self.get_duplicated_hosts()
4101 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004102 yield ('The following hosts were defined twice',
4103 self._format_hosts(dups),
4104 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004105
4106 partial = self.get_partially_configured_hosts()
4107 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004108 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4109 'These hosts are missing',
4110 self._format_hosts(partial, lambda host: 'but %s defined' %
4111 self._get_counterpart_host(host)),
4112 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004113
4114 conflicting = self.get_conflicting_hosts()
4115 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004116 yield ('The following Git hosts have differing credentials from their '
4117 'Gerrit counterparts',
4118 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4119 tuple(self._get_git_gerrit_identity_pairs()[host])),
4120 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004121
4122 wrong = self.get_hosts_with_wrong_identities()
4123 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004124 yield ('These hosts likely use wrong identity',
4125 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4126 (self._get_git_gerrit_identity_pairs()[host][0],
4127 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4128 wrong)
4129
4130 def find_and_report_problems(self):
4131 """Returns True if there was at least one problem, else False."""
4132 found = False
4133 bad_hosts = set()
4134 for title, sublines, hosts in self._find_problems():
4135 if not found:
4136 found = True
4137 print('\n\n.gitcookies problem report:\n')
4138 bad_hosts.update(hosts or [])
4139 print(' %s%s' % (title , (':' if sublines else '')))
4140 if sublines:
4141 print()
4142 print(' %s' % '\n '.join(sublines))
4143 print()
4144
4145 if bad_hosts:
4146 assert found
4147 print(' You can manually remove corresponding lines in your %s file and '
4148 'visit the following URLs with correct account to generate '
4149 'correct credential lines:\n' %
4150 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4151 print(' %s' % '\n '.join(sorted(set(
4152 gerrit_util.CookiesAuthenticator().get_new_password_url(
4153 self._canonical_git_googlesource_host(host))
4154 for host in bad_hosts
4155 ))))
4156 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004157
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004158
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004159@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004160def CMDcreds_check(parser, args):
4161 """Checks credentials and suggests changes."""
4162 _, _ = parser.parse_args(args)
4163
4164 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004165 DieWithError(
4166 'This command is not designed for GCE, are you on a bot?\n'
4167 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004168
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004169 checker = _GitCookiesChecker()
4170 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004171
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004172 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004173 checker.print_current_creds(include_netrc=True)
4174
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004175 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004176 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004177 return 0
4178 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004179
4180
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004181@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004182@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004183def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004184 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004185
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004186 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004187 # TODO(tandrii): remove this once we switch to Gerrit.
4188 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004189 parser.add_option('--activate-update', action='store_true',
4190 help='activate auto-updating [rietveld] section in '
4191 '.git/config')
4192 parser.add_option('--deactivate-update', action='store_true',
4193 help='deactivate auto-updating [rietveld] section in '
4194 '.git/config')
4195 options, args = parser.parse_args(args)
4196
4197 if options.deactivate_update:
4198 RunGit(['config', 'rietveld.autoupdate', 'false'])
4199 return
4200
4201 if options.activate_update:
4202 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4203 return
4204
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004205 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004206 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004207 return 0
4208
4209 url = args[0]
4210 if not url.endswith('codereview.settings'):
4211 url = os.path.join(url, 'codereview.settings')
4212
4213 # Load code review settings and download hooks (if available).
4214 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4215 return 0
4216
4217
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004218@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004219def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004220 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004221 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4222 branch = ShortBranchName(branchref)
4223 _, args = parser.parse_args(args)
4224 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004225 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004226 return RunGit(['config', 'branch.%s.base-url' % branch],
4227 error_ok=False).strip()
4228 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004229 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004230 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4231 error_ok=False).strip()
4232
4233
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004234def color_for_status(status):
4235 """Maps a Changelist status to color, for CMDstatus and other tools."""
4236 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004237 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004238 'waiting': Fore.BLUE,
4239 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004240 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004241 'lgtm': Fore.GREEN,
4242 'commit': Fore.MAGENTA,
4243 'closed': Fore.CYAN,
4244 'error': Fore.WHITE,
4245 }.get(status, Fore.WHITE)
4246
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004247
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004248def get_cl_statuses(changes, fine_grained, max_processes=None):
4249 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004250
4251 If fine_grained is true, this will fetch CL statuses from the server.
4252 Otherwise, simply indicate if there's a matching url for the given branches.
4253
4254 If max_processes is specified, it is used as the maximum number of processes
4255 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4256 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004257
4258 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004259 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004260 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004261 upload.verbosity = 0
4262
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004263 if not changes:
4264 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004265
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004266 if not fine_grained:
4267 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004268 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004269 for cl in changes:
4270 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004271 return
4272
4273 # First, sort out authentication issues.
4274 logging.debug('ensuring credentials exist')
4275 for cl in changes:
4276 cl.EnsureAuthenticated(force=False, refresh=True)
4277
4278 def fetch(cl):
4279 try:
4280 return (cl, cl.GetStatus())
4281 except:
4282 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004283 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004284 raise
4285
4286 threads_count = len(changes)
4287 if max_processes:
4288 threads_count = max(1, min(threads_count, max_processes))
4289 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4290
4291 pool = ThreadPool(threads_count)
4292 fetched_cls = set()
4293 try:
4294 it = pool.imap_unordered(fetch, changes).__iter__()
4295 while True:
4296 try:
4297 cl, status = it.next(timeout=5)
4298 except multiprocessing.TimeoutError:
4299 break
4300 fetched_cls.add(cl)
4301 yield cl, status
4302 finally:
4303 pool.close()
4304
4305 # Add any branches that failed to fetch.
4306 for cl in set(changes) - fetched_cls:
4307 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004308
rmistry@google.com2dd99862015-06-22 12:22:18 +00004309
4310def upload_branch_deps(cl, args):
4311 """Uploads CLs of local branches that are dependents of the current branch.
4312
4313 If the local branch dependency tree looks like:
4314 test1 -> test2.1 -> test3.1
4315 -> test3.2
4316 -> test2.2 -> test3.3
4317
4318 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4319 run on the dependent branches in this order:
4320 test2.1, test3.1, test3.2, test2.2, test3.3
4321
4322 Note: This function does not rebase your local dependent branches. Use it when
4323 you make a change to the parent branch that will not conflict with its
4324 dependent branches, and you would like their dependencies updated in
4325 Rietveld.
4326 """
4327 if git_common.is_dirty_git_tree('upload-branch-deps'):
4328 return 1
4329
4330 root_branch = cl.GetBranch()
4331 if root_branch is None:
4332 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4333 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004334 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004335 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4336 'patchset dependencies without an uploaded CL.')
4337
4338 branches = RunGit(['for-each-ref',
4339 '--format=%(refname:short) %(upstream:short)',
4340 'refs/heads'])
4341 if not branches:
4342 print('No local branches found.')
4343 return 0
4344
4345 # Create a dictionary of all local branches to the branches that are dependent
4346 # on it.
4347 tracked_to_dependents = collections.defaultdict(list)
4348 for b in branches.splitlines():
4349 tokens = b.split()
4350 if len(tokens) == 2:
4351 branch_name, tracked = tokens
4352 tracked_to_dependents[tracked].append(branch_name)
4353
vapiera7fbd5a2016-06-16 09:17:49 -07004354 print()
4355 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004356 dependents = []
4357 def traverse_dependents_preorder(branch, padding=''):
4358 dependents_to_process = tracked_to_dependents.get(branch, [])
4359 padding += ' '
4360 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004361 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004362 dependents.append(dependent)
4363 traverse_dependents_preorder(dependent, padding)
4364 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004365 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004366
4367 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004368 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004369 return 0
4370
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004371 confirm_or_exit('This command will checkout all dependent branches and run '
4372 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004373
andybons@chromium.org962f9462016-02-03 20:00:42 +00004374 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004375 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004376 args.extend(['-t', 'Updated patchset dependency'])
4377
rmistry@google.com2dd99862015-06-22 12:22:18 +00004378 # Record all dependents that failed to upload.
4379 failures = {}
4380 # Go through all dependents, checkout the branch and upload.
4381 try:
4382 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004383 print()
4384 print('--------------------------------------')
4385 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004386 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004387 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004388 try:
4389 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004390 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004391 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004392 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004393 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004394 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004395 finally:
4396 # Swap back to the original root branch.
4397 RunGit(['checkout', '-q', root_branch])
4398
vapiera7fbd5a2016-06-16 09:17:49 -07004399 print()
4400 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004401 for dependent_branch in dependents:
4402 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004403 print(' %s : %s' % (dependent_branch, upload_status))
4404 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004405
4406 return 0
4407
4408
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004409@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004410def CMDarchive(parser, args):
4411 """Archives and deletes branches associated with closed changelists."""
4412 parser.add_option(
4413 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004414 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004415 parser.add_option(
4416 '-f', '--force', action='store_true',
4417 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004418 parser.add_option(
4419 '-d', '--dry-run', action='store_true',
4420 help='Skip the branch tagging and removal steps.')
4421 parser.add_option(
4422 '-t', '--notags', action='store_true',
4423 help='Do not tag archived branches. '
4424 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004425
4426 auth.add_auth_options(parser)
4427 options, args = parser.parse_args(args)
4428 if args:
4429 parser.error('Unsupported args: %s' % ' '.join(args))
4430 auth_config = auth.extract_auth_config_from_options(options)
4431
4432 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4433 if not branches:
4434 return 0
4435
vapiera7fbd5a2016-06-16 09:17:49 -07004436 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004437 changes = [Changelist(branchref=b, auth_config=auth_config)
4438 for b in branches.splitlines()]
4439 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4440 statuses = get_cl_statuses(changes,
4441 fine_grained=True,
4442 max_processes=options.maxjobs)
4443 proposal = [(cl.GetBranch(),
4444 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4445 for cl, status in statuses
4446 if status == 'closed']
4447 proposal.sort()
4448
4449 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004450 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004451 return 0
4452
4453 current_branch = GetCurrentBranch()
4454
vapiera7fbd5a2016-06-16 09:17:49 -07004455 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004456 if options.notags:
4457 for next_item in proposal:
4458 print(' ' + next_item[0])
4459 else:
4460 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4461 for next_item in proposal:
4462 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004463
kmarshall9249e012016-08-23 12:02:16 -07004464 # Quit now on precondition failure or if instructed by the user, either
4465 # via an interactive prompt or by command line flags.
4466 if options.dry_run:
4467 print('\nNo changes were made (dry run).\n')
4468 return 0
4469 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004470 print('You are currently on a branch \'%s\' which is associated with a '
4471 'closed codereview issue, so archive cannot proceed. Please '
4472 'checkout another branch and run this command again.' %
4473 current_branch)
4474 return 1
kmarshall9249e012016-08-23 12:02:16 -07004475 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004476 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4477 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004478 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004479 return 1
4480
4481 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004482 if not options.notags:
4483 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004484 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004485
vapiera7fbd5a2016-06-16 09:17:49 -07004486 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004487
4488 return 0
4489
4490
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004491@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004492def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004493 """Show status of changelists.
4494
4495 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004496 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004497 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004498 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004499 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004500 - Magenta in the commit queue
4501 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004502 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004503
4504 Also see 'git cl comments'.
4505 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004506 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004507 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004508 parser.add_option('-f', '--fast', action='store_true',
4509 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004510 parser.add_option(
4511 '-j', '--maxjobs', action='store', type=int,
4512 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004513
4514 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004515 _add_codereview_issue_select_options(
4516 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004517 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004518 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004519 if args:
4520 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004521 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004522
iannuccie53c9352016-08-17 14:40:40 -07004523 if options.issue is not None and not options.field:
4524 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004525
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004526 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004527 cl = Changelist(auth_config=auth_config, issue=options.issue,
4528 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004529 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004530 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004531 elif options.field == 'id':
4532 issueid = cl.GetIssue()
4533 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004534 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004535 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004536 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004538 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004539 elif options.field == 'status':
4540 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004541 elif options.field == 'url':
4542 url = cl.GetIssueURL()
4543 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004544 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004545 return 0
4546
4547 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4548 if not branches:
4549 print('No local branch found.')
4550 return 0
4551
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004552 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004553 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004554 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004555 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004556 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004557 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004558 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004559
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004560 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004561 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4562 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4563 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004564 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004565 c, status = output.next()
4566 branch_statuses[c.GetBranch()] = status
4567 status = branch_statuses.pop(branch)
4568 url = cl.GetIssueURL()
4569 if url and (not status or status == 'error'):
4570 # The issue probably doesn't exist anymore.
4571 url += ' (broken)'
4572
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004573 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004574 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004575 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004576 color = ''
4577 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004578 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004579 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004580 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004581 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004582
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004583
4584 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004585 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004586 print('Current branch: %s' % branch)
4587 for cl in changes:
4588 if cl.GetBranch() == branch:
4589 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004590 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004591 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004592 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004593 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004594 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004595 print('Issue description:')
4596 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004597 return 0
4598
4599
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004600def colorize_CMDstatus_doc():
4601 """To be called once in main() to add colors to git cl status help."""
4602 colors = [i for i in dir(Fore) if i[0].isupper()]
4603
4604 def colorize_line(line):
4605 for color in colors:
4606 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004607 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004608 indent = len(line) - len(line.lstrip(' ')) + 1
4609 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4610 return line
4611
4612 lines = CMDstatus.__doc__.splitlines()
4613 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4614
4615
phajdan.jre328cf92016-08-22 04:12:17 -07004616def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004617 if path == '-':
4618 json.dump(contents, sys.stdout)
4619 else:
4620 with open(path, 'w') as f:
4621 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004622
4623
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004624@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004625@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004626def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004627 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004628
4629 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004630 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004631 parser.add_option('-r', '--reverse', action='store_true',
4632 help='Lookup the branch(es) for the specified issues. If '
4633 'no issues are specified, all branches with mapped '
4634 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004635 parser.add_option('--json',
4636 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004637 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004638 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004639 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004640
dnj@chromium.org406c4402015-03-03 17:22:28 +00004641 if options.reverse:
4642 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004643 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004644 # Reverse issue lookup.
4645 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004646
4647 git_config = {}
4648 for config in RunGit(['config', '--get-regexp',
4649 r'branch\..*issue']).splitlines():
4650 name, _space, val = config.partition(' ')
4651 git_config[name] = val
4652
dnj@chromium.org406c4402015-03-03 17:22:28 +00004653 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004654 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4655 config_key = _git_branch_config_key(ShortBranchName(branch),
4656 cls.IssueConfigKey())
4657 issue = git_config.get(config_key)
4658 if issue:
4659 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004660 if not args:
4661 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004662 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004663 for issue in args:
4664 if not issue:
4665 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004666 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004667 print('Branch for issue number %s: %s' % (
4668 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004669 if options.json:
4670 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004671 return 0
4672
4673 if len(args) > 0:
4674 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4675 if not issue.valid:
4676 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4677 'or no argument to list it.\n'
4678 'Maybe you want to run git cl status?')
4679 cl = Changelist(codereview=issue.codereview)
4680 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004681 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004682 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004683 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4684 if options.json:
4685 write_json(options.json, {
4686 'issue': cl.GetIssue(),
4687 'issue_url': cl.GetIssueURL(),
4688 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004689 return 0
4690
4691
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004692@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004693def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004694 """Shows or posts review comments for any changelist."""
4695 parser.add_option('-a', '--add-comment', dest='comment',
4696 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004697 parser.add_option('-i', '--issue', dest='issue',
4698 help='review issue id (defaults to current issue). '
4699 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004700 parser.add_option('-m', '--machine-readable', dest='readable',
4701 action='store_false', default=True,
4702 help='output comments in a format compatible with '
4703 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004704 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004705 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004706 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004707 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004708 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004709 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004710 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004711
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004712 issue = None
4713 if options.issue:
4714 try:
4715 issue = int(options.issue)
4716 except ValueError:
4717 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004718 if not options.forced_codereview:
4719 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004720
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004721 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004722 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004723 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004724
4725 if options.comment:
4726 cl.AddComment(options.comment)
4727 return 0
4728
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004729 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4730 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004731 for comment in summary:
4732 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004733 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004734 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004735 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004736 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004737 color = Fore.MAGENTA
4738 else:
4739 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004740 print('\n%s%s %s%s\n%s' % (
4741 color,
4742 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4743 comment.sender,
4744 Fore.RESET,
4745 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4746
smut@google.comc85ac942015-09-15 16:34:43 +00004747 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004748 def pre_serialize(c):
4749 dct = c.__dict__.copy()
4750 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4751 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004752 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004753 return 0
4754
4755
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004756@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004757@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004758def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004759 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004760 parser.add_option('-d', '--display', action='store_true',
4761 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004762 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004763 help='New description to set for this issue (- for stdin, '
4764 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004765 parser.add_option('-f', '--force', action='store_true',
4766 help='Delete any unpublished Gerrit edits for this issue '
4767 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004768
4769 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004770 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004771 options, args = parser.parse_args(args)
4772 _process_codereview_select_options(parser, options)
4773
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004774 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004775 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004776 target_issue_arg = ParseIssueNumberArgument(args[0],
4777 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004778 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004779 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004780
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004781 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004782
martiniss6eda05f2016-06-30 10:18:35 -07004783 kwargs = {
4784 'auth_config': auth_config,
4785 'codereview': options.forced_codereview,
4786 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004787 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004788 if target_issue_arg:
4789 kwargs['issue'] = target_issue_arg.issue
4790 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004791 if target_issue_arg.codereview and not options.forced_codereview:
4792 detected_codereview_from_url = True
4793 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004794
4795 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004796 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004797 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004798 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004799
4800 if detected_codereview_from_url:
4801 logging.info('canonical issue/change URL: %s (type: %s)\n',
4802 cl.GetIssueURL(), target_issue_arg.codereview)
4803
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004804 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004805
smut@google.com34fb6b12015-07-13 20:03:26 +00004806 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004807 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004808 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004809
4810 if options.new_description:
4811 text = options.new_description
4812 if text == '-':
4813 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004814 elif text == '+':
4815 base_branch = cl.GetCommonAncestorWithUpstream()
4816 change = cl.GetChange(base_branch, None, local_description=True)
4817 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004818
4819 description.set_description(text)
4820 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004821 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004822
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004823 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004824 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004825 return 0
4826
4827
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004828def CreateDescriptionFromLog(args):
4829 """Pulls out the commit log to use as a base for the CL description."""
4830 log_args = []
4831 if len(args) == 1 and not args[0].endswith('.'):
4832 log_args = [args[0] + '..']
4833 elif len(args) == 1 and args[0].endswith('...'):
4834 log_args = [args[0][:-1]]
4835 elif len(args) == 2:
4836 log_args = [args[0] + '..' + args[1]]
4837 else:
4838 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004839 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004840
4841
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004842@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004843def CMDlint(parser, args):
4844 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004845 parser.add_option('--filter', action='append', metavar='-x,+y',
4846 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004847 auth.add_auth_options(parser)
4848 options, args = parser.parse_args(args)
4849 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004850
4851 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004852 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004853 try:
4854 import cpplint
4855 import cpplint_chromium
4856 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004857 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004858 return 1
4859
4860 # Change the current working directory before calling lint so that it
4861 # shows the correct base.
4862 previous_cwd = os.getcwd()
4863 os.chdir(settings.GetRoot())
4864 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004865 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004866 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4867 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004868 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004869 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004870 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004871
4872 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004873 command = args + files
4874 if options.filter:
4875 command = ['--filter=' + ','.join(options.filter)] + command
4876 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004877
4878 white_regex = re.compile(settings.GetLintRegex())
4879 black_regex = re.compile(settings.GetLintIgnoreRegex())
4880 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4881 for filename in filenames:
4882 if white_regex.match(filename):
4883 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004884 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004885 else:
4886 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4887 extra_check_functions)
4888 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004889 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004890 finally:
4891 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004892 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004893 if cpplint._cpplint_state.error_count != 0:
4894 return 1
4895 return 0
4896
4897
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004898@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004899def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004900 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004901 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004902 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004903 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004904 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004905 parser.add_option('--all', action='store_true',
4906 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004907 parser.add_option('--parallel', action='store_true',
4908 help='Run all tests specified by input_api.RunTests in all '
4909 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004910 auth.add_auth_options(parser)
4911 options, args = parser.parse_args(args)
4912 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004913
sbc@chromium.org71437c02015-04-09 19:29:40 +00004914 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004915 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004916 return 1
4917
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004918 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004919 if args:
4920 base_branch = args[0]
4921 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004922 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004923 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004924
Aaron Gable8076c282017-11-29 14:39:41 -08004925 if options.all:
4926 base_change = cl.GetChange(base_branch, None)
4927 files = [('M', f) for f in base_change.AllFiles()]
4928 change = presubmit_support.GitChange(
4929 base_change.Name(),
4930 base_change.FullDescriptionText(),
4931 base_change.RepositoryRoot(),
4932 files,
4933 base_change.issue,
4934 base_change.patchset,
4935 base_change.author_email,
4936 base_change._upstream)
4937 else:
4938 change = cl.GetChange(base_branch, None)
4939
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004940 cl.RunHook(
4941 committing=not options.upload,
4942 may_prompt=False,
4943 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004944 change=change,
4945 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004946 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004947
4948
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004949def GenerateGerritChangeId(message):
4950 """Returns Ixxxxxx...xxx change id.
4951
4952 Works the same way as
4953 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4954 but can be called on demand on all platforms.
4955
4956 The basic idea is to generate git hash of a state of the tree, original commit
4957 message, author/committer info and timestamps.
4958 """
4959 lines = []
4960 tree_hash = RunGitSilent(['write-tree'])
4961 lines.append('tree %s' % tree_hash.strip())
4962 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4963 if code == 0:
4964 lines.append('parent %s' % parent.strip())
4965 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4966 lines.append('author %s' % author.strip())
4967 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4968 lines.append('committer %s' % committer.strip())
4969 lines.append('')
4970 # Note: Gerrit's commit-hook actually cleans message of some lines and
4971 # whitespace. This code is not doing this, but it clearly won't decrease
4972 # entropy.
4973 lines.append(message)
4974 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4975 stdin='\n'.join(lines))
4976 return 'I%s' % change_hash.strip()
4977
4978
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004979def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004980 """Computes the remote branch ref to use for the CL.
4981
4982 Args:
4983 remote (str): The git remote for the CL.
4984 remote_branch (str): The git remote branch for the CL.
4985 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004986 """
4987 if not (remote and remote_branch):
4988 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004989
wittman@chromium.org455dc922015-01-26 20:15:50 +00004990 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004991 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004992 # refs, which are then translated into the remote full symbolic refs
4993 # below.
4994 if '/' not in target_branch:
4995 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4996 else:
4997 prefix_replacements = (
4998 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4999 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
5000 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
5001 )
5002 match = None
5003 for regex, replacement in prefix_replacements:
5004 match = re.search(regex, target_branch)
5005 if match:
5006 remote_branch = target_branch.replace(match.group(0), replacement)
5007 break
5008 if not match:
5009 # This is a branch path but not one we recognize; use as-is.
5010 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00005011 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
5012 # Handle the refs that need to land in different refs.
5013 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005014
wittman@chromium.org455dc922015-01-26 20:15:50 +00005015 # Create the true path to the remote branch.
5016 # Does the following translation:
5017 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
5018 # * refs/remotes/origin/master -> refs/heads/master
5019 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
5020 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
5021 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
5022 elif remote_branch.startswith('refs/remotes/%s/' % remote):
5023 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
5024 'refs/heads/')
5025 elif remote_branch.startswith('refs/remotes/branch-heads'):
5026 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01005027
wittman@chromium.org455dc922015-01-26 20:15:50 +00005028 return remote_branch
5029
5030
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005031def cleanup_list(l):
5032 """Fixes a list so that comma separated items are put as individual items.
5033
5034 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
5035 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
5036 """
5037 items = sum((i.split(',') for i in l), [])
5038 stripped_items = (i.strip() for i in items)
5039 return sorted(filter(None, stripped_items))
5040
5041
Aaron Gable4db38df2017-11-03 14:59:07 -07005042@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005043@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005044def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00005045 """Uploads the current changelist to codereview.
5046
5047 Can skip dependency patchset uploads for a branch by running:
5048 git config branch.branch_name.skip-deps-uploads True
5049 To unset run:
5050 git config --unset branch.branch_name.skip-deps-uploads
5051 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02005052
5053 If the name of the checked out branch starts with "bug-" or "fix-" followed by
5054 a bug number, this bug number is automatically populated in the CL
5055 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005056
5057 If subject contains text in square brackets or has "<text>: " prefix, such
5058 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
5059 [git-cl] add support for hashtags
5060 Foo bar: implement foo
5061 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00005062 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00005063 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5064 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00005065 parser.add_option('--bypass-watchlists', action='store_true',
5066 dest='bypass_watchlists',
5067 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07005068 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00005069 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005070 parser.add_option('--message', '-m', dest='message',
5071 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07005072 parser.add_option('-b', '--bug',
5073 help='pre-populate the bug number(s) for this issue. '
5074 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07005075 parser.add_option('--message-file', dest='message_file',
5076 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005077 parser.add_option('--title', '-t', dest='title',
5078 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005079 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005080 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00005081 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005082 parser.add_option('--tbrs',
5083 action='append', default=[],
5084 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005085 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005086 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00005087 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005088 parser.add_option('--hashtag', dest='hashtags',
5089 action='append', default=[],
5090 help=('Gerrit hashtag for new CL; '
5091 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00005092 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08005093 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005094 parser.add_option('--emulate_svn_auto_props',
5095 '--emulate-svn-auto-props',
5096 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00005097 dest="emulate_svn_auto_props",
5098 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00005099 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07005100 help='tell the commit queue to commit this patchset; '
5101 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00005102 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005103 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00005104 metavar='TARGET',
5105 help='Apply CL to remote ref TARGET. ' +
5106 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005107 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005108 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00005109 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005110 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005111 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005112 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005113 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5114 const='TBR', help='add a set of OWNERS to TBR')
5115 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5116 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005117 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5118 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005119 help='Send the patchset to do a CQ dry run right after '
5120 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005121 parser.add_option('--dependencies', action='store_true',
5122 help='Uploads CLs of all the local branches that depend on '
5123 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04005124 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5125 help='Sends your change to the CQ after an approval. Only '
5126 'works on repos that have the Auto-Submit label '
5127 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04005128 parser.add_option('--parallel', action='store_true',
5129 help='Run all tests specified by input_api.RunTests in all '
5130 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005131
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005132 parser.add_option('--no-autocc', action='store_true',
5133 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005134 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005135 help='Set the review private. This implies --no-autocc.')
5136
5137 # TODO: remove Rietveld flags
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005138 parser.add_option('--email', default=None,
5139 help='email address to use to connect to Rietveld')
5140
rmistry@google.com2dd99862015-06-22 12:22:18 +00005141 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005142 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005143 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005144 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005145 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005146 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005147
sbc@chromium.org71437c02015-04-09 19:29:40 +00005148 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005149 return 1
5150
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005151 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005152 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005153 options.cc = cleanup_list(options.cc)
5154
tandriib80458a2016-06-23 12:20:07 -07005155 if options.message_file:
5156 if options.message:
5157 parser.error('only one of --message and --message-file allowed.')
5158 options.message = gclient_utils.FileRead(options.message_file)
5159 options.message_file = None
5160
tandrii4d0545a2016-07-06 03:56:49 -07005161 if options.cq_dry_run and options.use_commit_queue:
5162 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5163
Aaron Gableedbc4132017-09-11 13:22:28 -07005164 if options.use_commit_queue:
5165 options.send_mail = True
5166
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005167 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5168 settings.GetIsGerrit()
5169
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005170 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005171 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005172
5173
Francois Dorayd42c6812017-05-30 15:10:20 -04005174@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005175@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005176def CMDsplit(parser, args):
5177 """Splits a branch into smaller branches and uploads CLs.
5178
5179 Creates a branch and uploads a CL for each group of files modified in the
5180 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005181 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005182 the shared OWNERS file.
5183 """
5184 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005185 help="A text file containing a CL description in which "
5186 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005187 parser.add_option("-c", "--comment", dest="comment_file",
5188 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005189 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5190 default=False,
5191 help="List the files and reviewers for each CL that would "
5192 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00005193 parser.add_option("--cq-dry-run", action='store_true',
5194 help="If set, will do a cq dry run for each uploaded CL. "
5195 "Please be careful when doing this; more than ~10 CLs "
5196 "has the potential to overload our build "
5197 "infrastructure. Try to upload these not during high "
5198 "load times (usually 11-3 Mountain View time). Email "
5199 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005200 options, _ = parser.parse_args(args)
5201
5202 if not options.description_file:
5203 parser.error('No --description flag specified.')
5204
5205 def WrappedCMDupload(args):
5206 return CMDupload(OptionParser(), args)
5207
5208 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00005209 Changelist, WrappedCMDupload, options.dry_run,
5210 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005211
5212
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005213@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005214@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005215def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005216 """DEPRECATED: Used to commit the current changelist via git-svn."""
5217 message = ('git-cl no longer supports committing to SVN repositories via '
5218 'git-svn. You probably want to use `git cl land` instead.')
5219 print(message)
5220 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005221
5222
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005223# Two special branches used by git cl land.
5224MERGE_BRANCH = 'git-cl-commit'
5225CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5226
5227
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005228@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005229@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005230def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005231 """Commits the current changelist via git.
5232
5233 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5234 upstream and closes the issue automatically and atomically.
5235
5236 Otherwise (in case of Rietveld):
5237 Squashes branch into a single commit.
5238 Updates commit message with metadata (e.g. pointer to review).
5239 Pushes the code upstream.
5240 Updates review and closes.
5241 """
5242 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5243 help='bypass upload presubmit hook')
5244 parser.add_option('-m', dest='message',
5245 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005246 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005247 help="force yes to questions (don't prompt)")
5248 parser.add_option('-c', dest='contributor',
5249 help="external contributor for patch (appended to " +
5250 "description and used as author for git). Should be " +
5251 "formatted as 'First Last <email@example.com>'")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005252 parser.add_option('--parallel', action='store_true',
5253 help='Run all tests specified by input_api.RunTests in all '
5254 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005255 auth.add_auth_options(parser)
5256 (options, args) = parser.parse_args(args)
5257 auth_config = auth.extract_auth_config_from_options(options)
5258
5259 cl = Changelist(auth_config=auth_config)
5260
Robert Iannucci2e73d432018-03-14 01:10:47 -07005261 if not cl.IsGerrit():
5262 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005263
Robert Iannucci2e73d432018-03-14 01:10:47 -07005264 if options.message:
5265 # This could be implemented, but it requires sending a new patch to
5266 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5267 # Besides, Gerrit has the ability to change the commit message on submit
5268 # automatically, thus there is no need to support this option (so far?).
5269 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005270 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005271 parser.error(
5272 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5273 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5274 'the contributor\'s "name <email>". If you can\'t upload such a '
5275 'commit for review, contact your repository admin and request'
5276 '"Forge-Author" permission.')
5277 if not cl.GetIssue():
5278 DieWithError('You must upload the change first to Gerrit.\n'
5279 ' If you would rather have `git cl land` upload '
5280 'automatically for you, see http://crbug.com/642759')
5281 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005282 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005283
5284
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005285def PushToGitWithAutoRebase(remote, branch, original_description,
5286 git_numberer_enabled, max_attempts=3):
5287 """Pushes current HEAD commit on top of remote's branch.
5288
5289 Attempts to fetch and autorebase on push failures.
5290 Adds git number footers on the fly.
5291
5292 Returns integer code from last command.
5293 """
5294 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5295 code = 0
5296 attempts_left = max_attempts
5297 while attempts_left:
5298 attempts_left -= 1
5299 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5300
5301 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5302 # If fetch fails, retry.
5303 print('Fetching %s/%s...' % (remote, branch))
5304 code, out = RunGitWithCode(
5305 ['retry', 'fetch', remote,
5306 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5307 if code:
5308 print('Fetch failed with exit code %d.' % code)
5309 print(out.strip())
5310 continue
5311
5312 print('Cherry-picking commit on top of latest %s' % branch)
5313 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5314 suppress_stderr=True)
5315 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5316 code, out = RunGitWithCode(['cherry-pick', cherry])
5317 if code:
5318 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5319 'the following files have merge conflicts:' %
5320 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005321 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5322 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005323 print('Please rebase your patch and try again.')
5324 RunGitWithCode(['cherry-pick', '--abort'])
5325 break
5326
5327 commit_desc = ChangeDescription(original_description)
5328 if git_numberer_enabled:
5329 logging.debug('Adding git number footers')
5330 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5331 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5332 branch)
5333 # Ensure timestamps are monotonically increasing.
5334 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5335 _get_committer_timestamp('HEAD'))
5336 _git_amend_head(commit_desc.description, timestamp)
5337
5338 code, out = RunGitWithCode(
5339 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5340 print(out)
5341 if code == 0:
5342 break
5343 if IsFatalPushFailure(out):
5344 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005345 'user.email are correct and you have push access to the repo.\n'
5346 'Hint: run command below to diangose common Git/Gerrit credential '
5347 'problems:\n'
5348 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005349 break
5350 return code
5351
5352
5353def IsFatalPushFailure(push_stdout):
5354 """True if retrying push won't help."""
5355 return '(prohibited by Gerrit)' in push_stdout
5356
5357
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005358@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005359@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005360def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005361 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005362 parser.add_option('-b', dest='newbranch',
5363 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005364 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005365 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005366 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005367 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005368 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005369 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005370 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005371 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005372 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005373 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005374
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005375
5376 group = optparse.OptionGroup(
5377 parser,
5378 'Options for continuing work on the current issue uploaded from a '
5379 'different clone (e.g. different machine). Must be used independently '
5380 'from the other options. No issue number should be specified, and the '
5381 'branch must have an issue number associated with it')
5382 group.add_option('--reapply', action='store_true', dest='reapply',
5383 help='Reset the branch and reapply the issue.\n'
5384 'CAUTION: This will undo any local changes in this '
5385 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005386
5387 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005388 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005389 parser.add_option_group(group)
5390
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005391 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005392 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005393 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005394 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005395 auth_config = auth.extract_auth_config_from_options(options)
5396
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005397 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005398 if options.newbranch:
5399 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005400 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005401 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005402
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005403 cl = Changelist(auth_config=auth_config,
5404 codereview=options.forced_codereview)
5405 if not cl.GetIssue():
5406 parser.error('current branch must have an associated issue')
5407
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005408 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005409 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005410 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005411
5412 RunGit(['reset', '--hard', upstream])
5413 if options.pull:
5414 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005415
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005416 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5417 options.directory)
5418
5419 if len(args) != 1 or not args[0]:
5420 parser.error('Must specify issue number or url')
5421
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005422 target_issue_arg = ParseIssueNumberArgument(args[0],
5423 options.forced_codereview)
5424 if not target_issue_arg.valid:
5425 parser.error('invalid codereview url or CL id')
5426
5427 cl_kwargs = {
5428 'auth_config': auth_config,
5429 'codereview_host': target_issue_arg.hostname,
5430 'codereview': options.forced_codereview,
5431 }
5432 detected_codereview_from_url = False
5433 if target_issue_arg.codereview and not options.forced_codereview:
5434 detected_codereview_from_url = True
5435 cl_kwargs['codereview'] = target_issue_arg.codereview
5436 cl_kwargs['issue'] = target_issue_arg.issue
5437
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005438 # We don't want uncommitted changes mixed up with the patch.
5439 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005440 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005441
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005442 if options.newbranch:
5443 if options.force:
5444 RunGit(['branch', '-D', options.newbranch],
5445 stderr=subprocess2.PIPE, error_ok=True)
5446 RunGit(['new-branch', options.newbranch])
5447
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005448 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005449
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005450 if cl.IsGerrit():
5451 if options.reject:
5452 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005453 if options.directory:
5454 parser.error('--directory is not supported with Gerrit codereview.')
5455
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005456 if detected_codereview_from_url:
5457 print('canonical issue/change URL: %s (type: %s)\n' %
5458 (cl.GetIssueURL(), target_issue_arg.codereview))
5459
5460 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005461 options.nocommit, options.directory,
5462 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005463
5464
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005465def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005466 """Fetches the tree status and returns either 'open', 'closed',
5467 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005468 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005469 if url:
5470 status = urllib2.urlopen(url).read().lower()
5471 if status.find('closed') != -1 or status == '0':
5472 return 'closed'
5473 elif status.find('open') != -1 or status == '1':
5474 return 'open'
5475 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005476 return 'unset'
5477
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005478
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005479def GetTreeStatusReason():
5480 """Fetches the tree status from a json url and returns the message
5481 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005482 url = settings.GetTreeStatusUrl()
5483 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005484 connection = urllib2.urlopen(json_url)
5485 status = json.loads(connection.read())
5486 connection.close()
5487 return status['message']
5488
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005489
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005490@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005491def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005492 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005493 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005494 status = GetTreeStatus()
5495 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005496 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005497 return 2
5498
vapiera7fbd5a2016-06-16 09:17:49 -07005499 print('The tree is %s' % status)
5500 print()
5501 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005502 if status != 'open':
5503 return 1
5504 return 0
5505
5506
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005507@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005508def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005509 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005510 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005511 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005512 '-b', '--bot', action='append',
5513 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5514 'times to specify multiple builders. ex: '
5515 '"-b win_rel -b win_layout". See '
5516 'the try server waterfall for the builders name and the tests '
5517 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005518 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005519 '-B', '--bucket', default='',
5520 help=('Buildbucket bucket to send the try requests.'))
5521 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005522 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005523 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005524 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005525 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005526 help='Revision to use for the try job; default: the revision will '
5527 'be determined by the try recipe that builder runs, which usually '
5528 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005529 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005530 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005531 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005532 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005533 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005534 '--category', default='git_cl_try', help='Specify custom build category.')
5535 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005536 '--project',
5537 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005538 'in recipe to determine to which repository or directory to '
5539 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005540 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005541 '-p', '--property', dest='properties', action='append', default=[],
5542 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005543 'key2=value2 etc. The value will be treated as '
5544 'json if decodable, or as string otherwise. '
5545 'NOTE: using this may make your try job not usable for CQ, '
5546 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +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.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005550 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005551 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005552 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005553 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005554 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005555 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005556
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005557 if options.master and options.master.startswith('luci.'):
5558 parser.error(
5559 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005560 # Make sure that all properties are prop=value pairs.
5561 bad_params = [x for x in options.properties if '=' not in x]
5562 if bad_params:
5563 parser.error('Got properties with missing "=": %s' % bad_params)
5564
maruel@chromium.org15192402012-09-06 12:38:29 +00005565 if args:
5566 parser.error('Unknown arguments: %s' % args)
5567
Koji Ishii31c14782018-01-08 17:17:33 +09005568 cl = Changelist(auth_config=auth_config, issue=options.issue,
5569 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005570 if not cl.GetIssue():
5571 parser.error('Need to upload first')
5572
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005573 if cl.IsGerrit():
5574 # HACK: warm up Gerrit change detail cache to save on RPCs.
5575 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5576
tandriie113dfd2016-10-11 10:20:12 -07005577 error_message = cl.CannotTriggerTryJobReason()
5578 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005579 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005580
borenet6c0efe62016-10-19 08:13:29 -07005581 if options.bucket and options.master:
5582 parser.error('Only one of --bucket and --master may be used.')
5583
qyearsley1fdfcb62016-10-24 13:22:03 -07005584 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005585
qyearsleydd49f942016-10-28 11:57:22 -07005586 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5587 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005588 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005589 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005590 print('git cl try with no bots now defaults to CQ dry run.')
5591 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5592 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005593
borenet6c0efe62016-10-19 08:13:29 -07005594 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005595 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005596 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005597 'of bot requires an initial job from a parent (usually a builder). '
5598 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005599 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005600 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005601
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005602 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005603 # TODO(tandrii): Checking local patchset against remote patchset is only
5604 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5605 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005606 print('Warning: Codereview server has newer patchsets (%s) than most '
5607 'recent upload from local checkout (%s). Did a previous upload '
5608 'fail?\n'
5609 'By default, git cl try uses the latest patchset from '
5610 'codereview, continuing to use patchset %s.\n' %
5611 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005612
tandrii568043b2016-10-11 07:49:18 -07005613 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005614 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005615 except BuildbucketResponseException as ex:
5616 print('ERROR: %s' % ex)
5617 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005618 return 0
5619
5620
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005621@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005622def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005623 """Prints info about try jobs associated with current CL."""
5624 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005625 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005626 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005627 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005628 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005629 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005630 '--color', action='store_true', default=setup_color.IS_TTY,
5631 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005632 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005633 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5634 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005635 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005636 '--json', help=('Path of JSON output file to write try job results to,'
5637 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005638 parser.add_option_group(group)
5639 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005640 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005641 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005642 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005643 if args:
5644 parser.error('Unrecognized args: %s' % ' '.join(args))
5645
5646 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005647 cl = Changelist(
5648 issue=options.issue, codereview=options.forced_codereview,
5649 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005650 if not cl.GetIssue():
5651 parser.error('Need to upload first')
5652
tandrii221ab252016-10-06 08:12:04 -07005653 patchset = options.patchset
5654 if not patchset:
5655 patchset = cl.GetMostRecentPatchset()
5656 if not patchset:
5657 parser.error('Codereview doesn\'t know about issue %s. '
5658 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005659 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005660 cl.GetIssue())
5661
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005662 # TODO(tandrii): Checking local patchset against remote patchset is only
5663 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5664 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005665 print('Warning: Codereview server has newer patchsets (%s) than most '
5666 'recent upload from local checkout (%s). Did a previous upload '
5667 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005668 'By default, git cl try-results uses the latest patchset from '
5669 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005670 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005671 try:
tandrii221ab252016-10-06 08:12:04 -07005672 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005673 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005674 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005675 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005676 if options.json:
5677 write_try_results_json(options.json, jobs)
5678 else:
5679 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005680 return 0
5681
5682
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005683@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005684@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005685def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005686 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005687 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005688 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005689 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005690
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005691 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005692 if args:
5693 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005694 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005695 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005696 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005697 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005698
5699 # Clear configured merge-base, if there is one.
5700 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005701 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005702 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005703 return 0
5704
5705
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005706@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005707def CMDweb(parser, args):
5708 """Opens the current CL in the web browser."""
5709 _, args = parser.parse_args(args)
5710 if args:
5711 parser.error('Unrecognized args: %s' % ' '.join(args))
5712
5713 issue_url = Changelist().GetIssueURL()
5714 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005715 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005716 return 1
5717
5718 webbrowser.open(issue_url)
5719 return 0
5720
5721
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005722@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005723def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005724 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005725 parser.add_option('-d', '--dry-run', action='store_true',
5726 help='trigger in dry run mode')
5727 parser.add_option('-c', '--clear', action='store_true',
5728 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005729 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005730 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005731 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005732 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005733 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005734 if args:
5735 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005736 if options.dry_run and options.clear:
5737 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5738
iannuccie53c9352016-08-17 14:40:40 -07005739 cl = Changelist(auth_config=auth_config, issue=options.issue,
5740 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005741 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005742 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005743 elif options.dry_run:
5744 state = _CQState.DRY_RUN
5745 else:
5746 state = _CQState.COMMIT
5747 if not cl.GetIssue():
5748 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005749 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005750 return 0
5751
5752
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005753@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005754def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005755 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005756 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005757 auth.add_auth_options(parser)
5758 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005759 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005760 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005761 if args:
5762 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005763 cl = Changelist(auth_config=auth_config, issue=options.issue,
5764 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005765 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005766 if not cl.GetIssue():
5767 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005768 cl.CloseIssue()
5769 return 0
5770
5771
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005772@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005773def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005774 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005775 parser.add_option(
5776 '--stat',
5777 action='store_true',
5778 dest='stat',
5779 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005780 auth.add_auth_options(parser)
5781 options, args = parser.parse_args(args)
5782 auth_config = auth.extract_auth_config_from_options(options)
5783 if args:
5784 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005785
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005786 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005787 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005788 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005789 if not issue:
5790 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005791
Aaron Gablea718c3e2017-08-28 17:47:28 -07005792 base = cl._GitGetBranchConfigValue('last-upload-hash')
5793 if not base:
5794 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5795 if not base:
5796 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5797 revision_info = detail['revisions'][detail['current_revision']]
5798 fetch_info = revision_info['fetch']['http']
5799 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5800 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005801
Aaron Gablea718c3e2017-08-28 17:47:28 -07005802 cmd = ['git', 'diff']
5803 if options.stat:
5804 cmd.append('--stat')
5805 cmd.append(base)
5806 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005807
5808 return 0
5809
5810
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005811@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005812def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005813 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005814 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005815 '--ignore-current',
5816 action='store_true',
5817 help='Ignore the CL\'s current reviewers and start from scratch.')
5818 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005819 '--no-color',
5820 action='store_true',
5821 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005822 parser.add_option(
5823 '--batch',
5824 action='store_true',
5825 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005826 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005827 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005828 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005829
5830 author = RunGit(['config', 'user.email']).strip() or None
5831
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005832 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005833
5834 if args:
5835 if len(args) > 1:
5836 parser.error('Unknown args')
5837 base_branch = args[0]
5838 else:
5839 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005840 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005841
5842 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005843 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5844
5845 if options.batch:
5846 db = owners.Database(change.RepositoryRoot(), file, os.path)
5847 print('\n'.join(db.reviewers_for(affected_files, author)))
5848 return 0
5849
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005850 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005851 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005852 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005853 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005854 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005855 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005856 disable_color=options.no_color,
5857 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005858
5859
Aiden Bennerc08566e2018-10-03 17:52:42 +00005860def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005861 """Generates a diff command."""
5862 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005863 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5864
5865 if not allow_prefix:
5866 diff_cmd += ['--no-prefix']
5867
5868 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005869
5870 if args:
5871 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005872 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005873 diff_cmd.append(arg)
5874 else:
5875 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005876
5877 return diff_cmd
5878
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005879
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005880def MatchingFileType(file_name, extensions):
5881 """Returns true if the file name ends with one of the given extensions."""
5882 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005883
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005884
enne@chromium.org555cfe42014-01-29 18:21:39 +00005885@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005886@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005887def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005888 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005889 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005890 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005891 parser.add_option('--full', action='store_true',
5892 help='Reformat the full content of all touched files')
5893 parser.add_option('--dry-run', action='store_true',
5894 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005895 parser.add_option('--python', action='store_true',
5896 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005897 parser.add_option('--js', action='store_true',
5898 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005899 parser.add_option('--diff', action='store_true',
5900 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005901 parser.add_option('--presubmit', action='store_true',
5902 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005903 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005904
Daniel Chengc55eecf2016-12-30 03:11:02 -08005905 # Normalize any remaining args against the current path, so paths relative to
5906 # the current directory are still resolved as expected.
5907 args = [os.path.join(os.getcwd(), arg) for arg in args]
5908
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005909 # git diff generates paths against the root of the repository. Change
5910 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005911 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005912 if rel_base_path:
5913 os.chdir(rel_base_path)
5914
digit@chromium.org29e47272013-05-17 17:01:46 +00005915 # Grab the merge-base commit, i.e. the upstream commit of the current
5916 # branch when it was created or the last time it was rebased. This is
5917 # to cover the case where the user may have called "git fetch origin",
5918 # moving the origin branch to a newer commit, but hasn't rebased yet.
5919 upstream_commit = None
5920 cl = Changelist()
5921 upstream_branch = cl.GetUpstreamBranch()
5922 if upstream_branch:
5923 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5924 upstream_commit = upstream_commit.strip()
5925
5926 if not upstream_commit:
5927 DieWithError('Could not find base commit for this branch. '
5928 'Are you in detached state?')
5929
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005930 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5931 diff_output = RunGit(changed_files_cmd)
5932 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005933 # Filter out files deleted by this CL
5934 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005935
Christopher Lamc5ba6922017-01-24 11:19:14 +11005936 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005937 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005938
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005939 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5940 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5941 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005942 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005943
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005944 top_dir = os.path.normpath(
5945 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5946
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005947 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5948 # formatted. This is used to block during the presubmit.
5949 return_value = 0
5950
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005951 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005952 # Locate the clang-format binary in the checkout
5953 try:
5954 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005955 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005956 DieWithError(e)
5957
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005958 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005959 cmd = [clang_format_tool]
5960 if not opts.dry_run and not opts.diff:
5961 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005962 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005963 if opts.diff:
5964 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005965 else:
5966 env = os.environ.copy()
5967 env['PATH'] = str(os.path.dirname(clang_format_tool))
5968 try:
5969 script = clang_format.FindClangFormatScriptInChromiumTree(
5970 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005971 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005972 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005973
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005974 cmd = [sys.executable, script, '-p0']
5975 if not opts.dry_run and not opts.diff:
5976 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005977
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005978 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5979 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005980
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005981 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5982 if opts.diff:
5983 sys.stdout.write(stdout)
5984 if opts.dry_run and len(stdout) > 0:
5985 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005986
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005987 # Similar code to above, but using yapf on .py files rather than clang-format
5988 # on C/C++ files
Aiden Bennerc08566e2018-10-03 17:52:42 +00005989 if opts.python and python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005990 yapf_tool = gclient_utils.FindExecutable('yapf')
5991 if yapf_tool is None:
5992 DieWithError('yapf not found in PATH')
5993
Aiden Bennerc08566e2018-10-03 17:52:42 +00005994 # If we couldn't find a yapf file we'll default to the chromium style
5995 # specified in depot_tools.
5996 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5997 chromium_default_yapf_style = os.path.join(depot_tools_path,
5998 YAPF_CONFIG_FILENAME)
5999
6000 # Note: yapf still seems to fix indentation of the entire file
6001 # even if line ranges are specified.
6002 # See https://github.com/google/yapf/issues/499
6003 if not opts.full:
6004 py_line_diffs = _ComputeDiffLineRanges(python_diff_files, upstream_commit)
6005
6006 # Used for caching.
6007 yapf_configs = {}
6008 for f in python_diff_files:
6009 # Find the yapf style config for the current file, defaults to depot
6010 # tools default.
6011 yapf_config = _FindYapfConfigFile(
6012 os.path.abspath(f), yapf_configs, top_dir,
6013 chromium_default_yapf_style)
6014
6015 cmd = [yapf_tool, '--style', yapf_config, f]
6016
6017 has_formattable_lines = False
6018 if not opts.full:
6019 # Only run yapf over changed line ranges.
6020 for diff_start, diff_len in py_line_diffs[f]:
6021 diff_end = diff_start + diff_len - 1
6022 # Yapf errors out if diff_end < diff_start but this
6023 # is a valid line range diff for a removal.
6024 if diff_end >= diff_start:
6025 has_formattable_lines = True
6026 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
6027 # If all line diffs were removals we have nothing to format.
6028 if not has_formattable_lines:
6029 continue
6030
6031 if opts.diff or opts.dry_run:
6032 cmd += ['--diff']
6033 # Will return non-zero exit code if non-empty diff.
6034 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
6035 if opts.diff:
6036 sys.stdout.write(stdout)
6037 elif len(stdout) > 0:
6038 return_value = 2
6039 else:
6040 cmd += ['-i']
6041 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006042
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006043 # Dart's formatter does not have the nice property of only operating on
6044 # modified chunks, so hard code full.
6045 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006046 try:
6047 command = [dart_format.FindDartFmtToolInChromiumTree()]
6048 if not opts.dry_run and not opts.diff:
6049 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006050 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006051
ppi@chromium.org6593d932016-03-03 15:41:15 +00006052 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006053 if opts.dry_run and stdout:
6054 return_value = 2
6055 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07006056 print('Warning: Unable to check Dart code formatting. Dart SDK not '
6057 'found in this checkout. Files in other languages are still '
6058 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006059
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006060 # Format GN build files. Always run on full build files for canonical form.
6061 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006062 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006063 if opts.dry_run or opts.diff:
6064 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006065 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006066 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6067 shell=sys.platform == 'win32',
6068 cwd=top_dir)
6069 if opts.dry_run and gn_ret == 2:
6070 return_value = 2 # Not formatted.
6071 elif opts.diff and gn_ret == 2:
6072 # TODO this should compute and print the actual diff.
6073 print("This change has GN build file diff for " + gn_diff_file)
6074 elif gn_ret != 0:
6075 # For non-dry run cases (and non-2 return values for dry-run), a
6076 # nonzero error code indicates a failure, probably because the file
6077 # doesn't parse.
6078 DieWithError("gn format failed on " + gn_diff_file +
6079 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006080
Ilya Shermane081cbe2017-08-15 17:51:04 -07006081 # Skip the metrics formatting from the global presubmit hook. These files have
6082 # a separate presubmit hook that issues an error if the files need formatting,
6083 # whereas the top-level presubmit script merely issues a warning. Formatting
6084 # these files is somewhat slow, so it's important not to duplicate the work.
6085 if not opts.presubmit:
6086 for xml_dir in GetDirtyMetricsDirs(diff_files):
6087 tool_dir = os.path.join(top_dir, xml_dir)
6088 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
6089 if opts.dry_run or opts.diff:
6090 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07006091 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07006092 if opts.diff:
6093 sys.stdout.write(stdout)
6094 if opts.dry_run and stdout:
6095 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006096
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006097 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006098
Steven Holte2e664bf2017-04-21 13:10:47 -07006099def GetDirtyMetricsDirs(diff_files):
6100 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
6101 metrics_xml_dirs = [
6102 os.path.join('tools', 'metrics', 'actions'),
6103 os.path.join('tools', 'metrics', 'histograms'),
6104 os.path.join('tools', 'metrics', 'rappor'),
6105 os.path.join('tools', 'metrics', 'ukm')]
6106 for xml_dir in metrics_xml_dirs:
6107 if any(file.startswith(xml_dir) for file in xml_diff_files):
6108 yield xml_dir
6109
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006110
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006111@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006112@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006113def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006114 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006115 _, args = parser.parse_args(args)
6116
6117 if len(args) != 1:
6118 parser.print_help()
6119 return 1
6120
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006121 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006122 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006123 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006124
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006125 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006126
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006127 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006128 output = RunGit(['config', '--local', '--get-regexp',
6129 r'branch\..*\.%s' % issueprefix],
6130 error_ok=True)
6131 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006132 if issue == target_issue:
6133 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006134
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006135 branches = []
6136 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006137 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006138 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006139 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006140 return 1
6141 if len(branches) == 1:
6142 RunGit(['checkout', branches[0]])
6143 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006144 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006145 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006146 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006147 which = raw_input('Choose by index: ')
6148 try:
6149 RunGit(['checkout', branches[int(which)]])
6150 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006151 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006152 return 1
6153
6154 return 0
6155
6156
maruel@chromium.org29404b52014-09-08 22:58:00 +00006157def CMDlol(parser, args):
6158 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006159 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006160 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6161 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6162 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006163 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006164 return 0
6165
6166
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006167class OptionParser(optparse.OptionParser):
6168 """Creates the option parse and add --verbose support."""
6169 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006170 optparse.OptionParser.__init__(
6171 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006172 self.add_option(
6173 '-v', '--verbose', action='count', default=0,
6174 help='Use 2 times for more debugging info')
6175
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006176 def parse_args(self, args=None, _values=None):
6177 # Create an optparse.Values object that will store only the actual passed
6178 # options, without the defaults.
6179 actual_options = optparse.Values()
6180 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6181 # Create an optparse.Values object with the default options.
6182 options = optparse.Values(self.get_default_values().__dict__)
6183 # Update it with the options passed by the user.
6184 options._update_careful(actual_options.__dict__)
6185 # Store the options passed by the user in an _actual_options attribute.
6186 # We store only the keys, and not the values, since the values can contain
6187 # arbitrary information, which might be PII.
6188 metrics.collector.add('arguments', actual_options.__dict__.keys())
6189
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006190 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006191 logging.basicConfig(
6192 level=levels[min(options.verbose, len(levels) - 1)],
6193 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6194 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006195 return options, args
6196
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006197
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006198def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006199 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006200 print('\nYour python version %s is unsupported, please upgrade.\n' %
6201 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006202 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006203
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006204 # Reload settings.
6205 global settings
6206 settings = Settings()
6207
Edward Lemurad463c92018-07-25 21:31:23 +00006208 if not metrics.DISABLE_METRICS_COLLECTION:
6209 metrics.collector.add('project_urls', [settings.GetViewVCUrl().strip('/+')])
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006210 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006211 dispatcher = subcommand.CommandDispatcher(__name__)
6212 try:
6213 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006214 except auth.AuthenticationError as e:
6215 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006216 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006217 if e.code != 500:
6218 raise
6219 DieWithError(
6220 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6221 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006222 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006223
6224
6225if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006226 # These affect sys.stdout so do it outside of main() to simplify mocks in
6227 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006228 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006229 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00006230 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00006231 sys.exit(main(sys.argv[1:]))