blob: 902e4e2eff22492ab29efc8cb80438176d0d2215 [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
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002491
2492 # Check presence of cookies only if using cookies-based auth method.
2493 cookie_auth = gerrit_util.Authenticator.get()
2494 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002495 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002496
2497 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002498 self.GetCodereviewServer()
2499 git_host = self._GetGitHost()
2500 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002501
2502 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2503 git_auth = cookie_auth.get_auth_header(git_host)
2504 if gerrit_auth and git_auth:
2505 if gerrit_auth == git_auth:
2506 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002507 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002508 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002509 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002510 ' %s\n'
2511 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002512 ' Consider running the following command:\n'
2513 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002514 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002515 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002516 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002517 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002518 cookie_auth.get_new_password_message(git_host)))
2519 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002520 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002521 return
2522 else:
2523 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002524 ([] if gerrit_auth else [self._gerrit_host]) +
2525 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002526 DieWithError('Credentials for the following hosts are required:\n'
2527 ' %s\n'
2528 'These are read from %s (or legacy %s)\n'
2529 '%s' % (
2530 '\n '.join(missing),
2531 cookie_auth.get_gitcookies_path(),
2532 cookie_auth.get_netrc_path(),
2533 cookie_auth.get_new_password_message(git_host)))
2534
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002535 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002536 if not self.GetIssue():
2537 return
2538
2539 # Warm change details cache now to avoid RPCs later, reducing latency for
2540 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002541 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002542 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002543
2544 status = self._GetChangeDetail()['status']
2545 if status in ('MERGED', 'ABANDONED'):
2546 DieWithError('Change %s has been %s, new uploads are not allowed' %
2547 (self.GetIssueURL(),
2548 'submitted' if status == 'MERGED' else 'abandoned'))
2549
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002550 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2551 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2552 # Apparently this check is not very important? Otherwise get_auth_email
2553 # could have been added to other implementations of Authenticator.
2554 cookies_auth = gerrit_util.Authenticator.get()
2555 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002556 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002557
2558 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002559 if self.GetIssueOwner() == cookies_user:
2560 return
2561 logging.debug('change %s owner is %s, cookies user is %s',
2562 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002563 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002564 # so ask what Gerrit thinks of this user.
2565 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2566 if details['email'] == self.GetIssueOwner():
2567 return
2568 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002569 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002570 'as %s.\n'
2571 'Uploading may fail due to lack of permissions.' %
2572 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2573 confirm_or_exit(action='upload')
2574
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002575 def _PostUnsetIssueProperties(self):
2576 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002577 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002578
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002579 def GetGerritObjForPresubmit(self):
2580 return presubmit_support.GerritAccessor(self._GetGerritHost())
2581
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002582 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002583 """Apply a rough heuristic to give a simple summary of an issue's review
2584 or CQ status, assuming adherence to a common workflow.
2585
2586 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002587 * 'error' - error from review tool (including deleted issues)
2588 * 'unsent' - no reviewers added
2589 * 'waiting' - waiting for review
2590 * 'reply' - waiting for uploader to reply to review
2591 * 'lgtm' - Code-Review label has been set
2592 * 'commit' - in the commit queue
2593 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002594 """
2595 if not self.GetIssue():
2596 return None
2597
2598 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002599 data = self._GetChangeDetail([
2600 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002601 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002602 return 'error'
2603
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002604 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002605 return 'closed'
2606
Aaron Gable9ab38c62017-04-06 14:36:33 -07002607 if data['labels'].get('Commit-Queue', {}).get('approved'):
2608 # The section will have an "approved" subsection if anyone has voted
2609 # the maximum value on the label.
2610 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002611
Aaron Gable9ab38c62017-04-06 14:36:33 -07002612 if data['labels'].get('Code-Review', {}).get('approved'):
2613 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002614
2615 if not data.get('reviewers', {}).get('REVIEWER', []):
2616 return 'unsent'
2617
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002618 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002619 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2620 last_message_author = messages.pop().get('author', {})
2621 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002622 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2623 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002624 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002625 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002626 if last_message_author.get('_account_id') == owner:
2627 # Most recent message was by owner.
2628 return 'waiting'
2629 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002630 # Some reply from non-owner.
2631 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002632
2633 # Somehow there are no messages even though there are reviewers.
2634 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002635
2636 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002637 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002638 patchset = data['revisions'][data['current_revision']]['_number']
2639 self.SetPatchset(patchset)
2640 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002641
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002642 def FetchDescription(self, force=False):
2643 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2644 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002645 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002646 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002647
dsansomee2d6fd92016-09-08 00:10:47 -07002648 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002649 if gerrit_util.HasPendingChangeEdit(
2650 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002651 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002652 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002653 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002654 'unpublished edit. Either publish the edit in the Gerrit web UI '
2655 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002656
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002657 gerrit_util.DeletePendingChangeEdit(
2658 self._GetGerritHost(), self._GerritChangeIdentifier())
2659 gerrit_util.SetCommitMessage(
2660 self._GetGerritHost(), self._GerritChangeIdentifier(),
2661 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002662
Aaron Gable636b13f2017-07-14 10:42:48 -07002663 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002664 gerrit_util.SetReview(
2665 self._GetGerritHost(), self._GerritChangeIdentifier(),
2666 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002667
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002668 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002669 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002670 messages = self._GetChangeDetail(
2671 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2672 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002673 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002674
2675 # Build dictionary of file comments for easy access and sorting later.
2676 # {author+date: {path: {patchset: {line: url+message}}}}
2677 comments = collections.defaultdict(
2678 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2679 for path, line_comments in file_comments.iteritems():
2680 for comment in line_comments:
2681 if comment.get('tag', '').startswith('autogenerated'):
2682 continue
2683 key = (comment['author']['email'], comment['updated'])
2684 if comment.get('side', 'REVISION') == 'PARENT':
2685 patchset = 'Base'
2686 else:
2687 patchset = 'PS%d' % comment['patch_set']
2688 line = comment.get('line', 0)
2689 url = ('https://%s/c/%s/%s/%s#%s%s' %
2690 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2691 'b' if comment.get('side') == 'PARENT' else '',
2692 str(line) if line else ''))
2693 comments[key][path][patchset][line] = (url, comment['message'])
2694
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002695 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002696 for msg in messages:
2697 # Don't bother showing autogenerated messages.
2698 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2699 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002700 # Gerrit spits out nanoseconds.
2701 assert len(msg['date'].split('.')[-1]) == 9
2702 date = datetime.datetime.strptime(msg['date'][:-3],
2703 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002704 message = msg['message']
2705 key = (msg['author']['email'], msg['date'])
2706 if key in comments:
2707 message += '\n'
2708 for path, patchsets in sorted(comments.get(key, {}).items()):
2709 if readable:
2710 message += '\n%s' % path
2711 for patchset, lines in sorted(patchsets.items()):
2712 for line, (url, content) in sorted(lines.items()):
2713 if line:
2714 line_str = 'Line %d' % line
2715 path_str = '%s:%d:' % (path, line)
2716 else:
2717 line_str = 'File comment'
2718 path_str = '%s:0:' % path
2719 if readable:
2720 message += '\n %s, %s: %s' % (patchset, line_str, url)
2721 message += '\n %s\n' % content
2722 else:
2723 message += '\n%s ' % path_str
2724 message += '\n%s\n' % content
2725
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002726 summary.append(_CommentSummary(
2727 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002728 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002729 sender=msg['author']['email'],
2730 # These could be inferred from the text messages and correlated with
2731 # Code-Review label maximum, however this is not reliable.
2732 # Leaving as is until the need arises.
2733 approval=False,
2734 disapproval=False,
2735 ))
2736 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002737
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002738 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002739 gerrit_util.AbandonChange(
2740 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002741
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002742 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002743 gerrit_util.SubmitChange(
2744 self._GetGerritHost(), self._GerritChangeIdentifier(),
2745 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002746
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002747 def _GetChangeDetail(self, options=None, no_cache=False):
2748 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002749
2750 If fresh data is needed, set no_cache=True which will clear cache and
2751 thus new data will be fetched from Gerrit.
2752 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002753 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002754 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002755
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002756 # Optimization to avoid multiple RPCs:
2757 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2758 'CURRENT_COMMIT' not in options):
2759 options.append('CURRENT_COMMIT')
2760
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002761 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002762 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002763 options = [o.upper() for o in options]
2764
2765 # Check in cache first unless no_cache is True.
2766 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002767 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002768 else:
2769 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002770 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002771 # Assumption: data fetched before with extra options is suitable
2772 # for return for a smaller set of options.
2773 # For example, if we cached data for
2774 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2775 # and request is for options=[CURRENT_REVISION],
2776 # THEN we can return prior cached data.
2777 if options_set.issubset(cached_options_set):
2778 return data
2779
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002780 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002781 data = gerrit_util.GetChangeDetail(
2782 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002783 except gerrit_util.GerritError as e:
2784 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002785 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002786 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002787
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002788 self._detail_cache.setdefault(cache_key, []).append(
2789 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002790 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002791
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002792 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002793 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002794 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002795 data = gerrit_util.GetChangeCommit(
2796 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002797 except gerrit_util.GerritError as e:
2798 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002799 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002800 raise
agable32978d92016-11-01 12:55:02 -07002801 return data
2802
Olivier Robin75ee7252018-04-13 10:02:56 +02002803 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002804 if git_common.is_dirty_git_tree('land'):
2805 return 1
tandriid60367b2016-06-22 05:25:12 -07002806 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2807 if u'Commit-Queue' in detail.get('labels', {}):
2808 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002809 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2810 'which can test and land changes for you. '
2811 'Are you sure you wish to bypass it?\n',
2812 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002813
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002814 differs = True
tandriic4344b52016-08-29 06:04:54 -07002815 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002816 # Note: git diff outputs nothing if there is no diff.
2817 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002818 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002819 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002820 if detail['current_revision'] == last_upload:
2821 differs = False
2822 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002823 print('WARNING: Local branch contents differ from latest uploaded '
2824 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002825 if differs:
2826 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002827 confirm_or_exit(
2828 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2829 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002830 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002831 elif not bypass_hooks:
2832 hook_results = self.RunHook(
2833 committing=True,
2834 may_prompt=not force,
2835 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002836 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2837 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002838 if not hook_results.should_continue():
2839 return 1
2840
2841 self.SubmitIssue(wait_for_merge=True)
2842 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002843 links = self._GetChangeCommit().get('web_links', [])
2844 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002845 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002846 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002847 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002848 return 0
2849
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002850 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002851 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002852 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002853 assert not directory
2854 assert parsed_issue_arg.valid
2855
2856 self._changelist.issue = parsed_issue_arg.issue
2857
2858 if parsed_issue_arg.hostname:
2859 self._gerrit_host = parsed_issue_arg.hostname
2860 self._gerrit_server = 'https://%s' % self._gerrit_host
2861
tandriic2405f52016-10-10 08:13:15 -07002862 try:
2863 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002864 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002865 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002866
2867 if not parsed_issue_arg.patchset:
2868 # Use current revision by default.
2869 revision_info = detail['revisions'][detail['current_revision']]
2870 patchset = int(revision_info['_number'])
2871 else:
2872 patchset = parsed_issue_arg.patchset
2873 for revision_info in detail['revisions'].itervalues():
2874 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2875 break
2876 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002877 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002878 (parsed_issue_arg.patchset, self.GetIssue()))
2879
Aaron Gable697a91b2018-01-19 15:20:15 -08002880 remote_url = self._changelist.GetRemoteUrl()
2881 if remote_url.endswith('.git'):
2882 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002883 remote_url = remote_url.rstrip('/')
2884
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002885 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002886 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002887
2888 if remote_url != fetch_info['url']:
2889 DieWithError('Trying to patch a change from %s but this repo appears '
2890 'to be %s.' % (fetch_info['url'], remote_url))
2891
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002892 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002893
Aaron Gable62619a32017-06-16 08:22:09 -07002894 if force:
2895 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2896 print('Checked out commit for change %i patchset %i locally' %
2897 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002898 elif nocommit:
2899 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2900 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002901 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002902 RunGit(['cherry-pick', 'FETCH_HEAD'])
2903 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002904 (parsed_issue_arg.issue, patchset))
2905 print('Note: this created a local commit which does not have '
2906 'the same hash as the one uploaded for review. This will make '
2907 'uploading changes based on top of this branch difficult.\n'
2908 'If you want to do that, use "git cl patch --force" instead.')
2909
Stefan Zagerd08043c2017-10-12 12:07:02 -07002910 if self.GetBranch():
2911 self.SetIssue(parsed_issue_arg.issue)
2912 self.SetPatchset(patchset)
2913 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2914 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2915 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2916 else:
2917 print('WARNING: You are in detached HEAD state.\n'
2918 'The patch has been applied to your checkout, but you will not be '
2919 'able to upload a new patch set to the gerrit issue.\n'
2920 'Try using the \'-b\' option if you would like to work on a '
2921 'branch and/or upload a new patch set.')
2922
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002923 return 0
2924
2925 @staticmethod
2926 def ParseIssueURL(parsed_url):
2927 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2928 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002929 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2930 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002931 # Short urls like https://domain/<issue_number> can be used, but don't allow
2932 # specifying the patchset (you'd 404), but we allow that here.
2933 if parsed_url.path == '/':
2934 part = parsed_url.fragment
2935 else:
2936 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002937 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002938 if match:
2939 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002940 issue=int(match.group(3)),
2941 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002942 hostname=parsed_url.netloc,
2943 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002944 return None
2945
tandrii16e0b4e2016-06-07 10:34:28 -07002946 def _GerritCommitMsgHookCheck(self, offer_removal):
2947 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2948 if not os.path.exists(hook):
2949 return
2950 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2951 # custom developer made one.
2952 data = gclient_utils.FileRead(hook)
2953 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2954 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002955 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002956 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002957 'and may interfere with it in subtle ways.\n'
2958 'We recommend you remove the commit-msg hook.')
2959 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002960 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002961 gclient_utils.rm_file_or_tree(hook)
2962 print('Gerrit commit-msg hook removed.')
2963 else:
2964 print('OK, will keep Gerrit commit-msg hook in place.')
2965
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002966 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002967 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002968 if options.squash and options.no_squash:
2969 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002970
2971 if not options.squash and not options.no_squash:
2972 # Load default for user, repo, squash=true, in this order.
2973 options.squash = settings.GetSquashGerritUploads()
2974 elif options.no_squash:
2975 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002976
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002977 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002978 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002979
Aaron Gableb56ad332017-01-06 15:24:31 -08002980 # This may be None; default fallback value is determined in logic below.
2981 title = options.title
2982
Dominic Battre7d1c4842017-10-27 09:17:28 +02002983 # Extract bug number from branch name.
2984 bug = options.bug
2985 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2986 if not bug and match:
2987 bug = match.group(1)
2988
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002989 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002990 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002991 if self.GetIssue():
2992 # Try to get the message from a previous upload.
2993 message = self.GetDescription()
2994 if not message:
2995 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002996 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002997 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002998 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002999 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07003000 # When uploading a subsequent patchset, -m|--message is taken
3001 # as the patchset title if --title was not provided.
3002 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003003 else:
3004 default_title = RunGit(
3005 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07003006 if options.force:
3007 title = default_title
3008 else:
3009 title = ask_for_data(
3010 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003011 change_id = self._GetChangeDetail()['change_id']
3012 while True:
3013 footer_change_ids = git_footers.get_footer_change_id(message)
3014 if footer_change_ids == [change_id]:
3015 break
3016 if not footer_change_ids:
3017 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003018 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003019 continue
3020 # There is already a valid footer but with different or several ids.
3021 # Doing this automatically is non-trivial as we don't want to lose
3022 # existing other footers, yet we want to append just 1 desired
3023 # Change-Id. Thus, just create a new footer, but let user verify the
3024 # new description.
3025 message = '%s\n\nChange-Id: %s' % (message, change_id)
3026 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08003027 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003028 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08003029 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003030 'Please, check the proposed correction to the description, '
3031 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
3032 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
3033 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003034 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003035 if not options.force:
3036 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02003037 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003038 message = change_desc.description
3039 if not message:
3040 DieWithError("Description is empty. Aborting...")
3041 # Continue the while loop.
3042 # Sanity check of this code - we should end up with proper message
3043 # footer.
3044 assert [change_id] == git_footers.get_footer_change_id(message)
3045 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08003046 else: # if not self.GetIssue()
3047 if options.message:
3048 message = options.message
3049 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003050 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08003051 if options.title:
3052 message = options.title + '\n\n' + message
3053 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003054
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003055 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02003056 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08003057 # On first upload, patchset title is always this string, while
3058 # --title flag gets converted to first line of message.
3059 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003060 if not change_desc.description:
3061 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003062 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003063 if len(change_ids) > 1:
3064 DieWithError('too many Change-Id footers, at most 1 allowed.')
3065 if not change_ids:
3066 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003067 change_desc.set_description(git_footers.add_footer_change_id(
3068 change_desc.description,
3069 GenerateGerritChangeId(change_desc.description)))
3070 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003071 assert len(change_ids) == 1
3072 change_id = change_ids[0]
3073
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003074 if options.reviewers or options.tbrs or options.add_owners_to:
3075 change_desc.update_reviewers(options.reviewers, options.tbrs,
3076 options.add_owners_to, change)
3077
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003078 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003079 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
3080 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003081 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07003082 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
3083 desc_tempfile.write(change_desc.description)
3084 desc_tempfile.close()
3085 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
3086 '-F', desc_tempfile.name]).strip()
3087 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003088 else:
3089 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003090 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003091 if not change_desc.description:
3092 DieWithError("Description is empty. Aborting...")
3093
3094 if not git_footers.get_footer_change_id(change_desc.description):
3095 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003096 change_desc.set_description(
3097 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003098 if options.reviewers or options.tbrs or options.add_owners_to:
3099 change_desc.update_reviewers(options.reviewers, options.tbrs,
3100 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003101 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003102 # For no-squash mode, we assume the remote called "origin" is the one we
3103 # want. It is not worthwhile to support different workflows for
3104 # no-squash mode.
3105 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003106 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3107
3108 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00003109 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003110 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3111 ref_to_push)]).splitlines()
3112 if len(commits) > 1:
3113 print('WARNING: This will upload %d commits. Run the following command '
3114 'to see which commits will be uploaded: ' % len(commits))
3115 print('git log %s..%s' % (parent, ref_to_push))
3116 print('You can also use `git squash-branch` to squash these into a '
3117 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003118 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003119
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003120 if options.reviewers or options.tbrs or options.add_owners_to:
3121 change_desc.update_reviewers(options.reviewers, options.tbrs,
3122 options.add_owners_to, change)
3123
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003124 # Extra options that can be specified at push time. Doc:
3125 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003126 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003127
Aaron Gable844cf292017-06-28 11:32:59 -07003128 # By default, new changes are started in WIP mode, and subsequent patchsets
3129 # don't send email. At any time, passing --send-mail will mark the change
3130 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003131 if options.send_mail:
3132 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003133 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04003134 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003135 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003136 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003137 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003138
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003139 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003140 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003141
Aaron Gable9b713dd2016-12-14 16:04:21 -08003142 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003143 # Punctuation and whitespace in |title| must be percent-encoded.
3144 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003145
agablec6787972016-09-09 16:13:34 -07003146 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003147 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003148
rmistry9eadede2016-09-19 11:22:43 -07003149 if options.topic:
3150 # Documentation on Gerrit topics is here:
3151 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003152 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003153
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003154 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003155 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003156 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003157 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003158 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3159
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003160 refspec_suffix = ''
3161 if refspec_opts:
3162 refspec_suffix = '%' + ','.join(refspec_opts)
3163 assert ' ' not in refspec_suffix, (
3164 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3165 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3166
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003167 try:
3168 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00003169 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003170 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003171 # Flush after every line: useful for seeing progress when running as
3172 # recipe.
3173 filter_fn=lambda _: sys.stdout.flush())
3174 except subprocess2.CalledProcessError:
3175 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003176 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003177 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003178 'credential problems:\n'
3179 ' git cl creds-check\n',
3180 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003181
3182 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003183 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003184 change_numbers = [m.group(1)
3185 for m in map(regex.match, push_stdout.splitlines())
3186 if m]
3187 if len(change_numbers) != 1:
3188 DieWithError(
3189 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003190 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003191 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003192 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003193
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003194 reviewers = sorted(change_desc.get_reviewers())
3195
tandrii88189772016-09-29 04:29:57 -07003196 # Add cc's from the CC_LIST and --cc flag (if any).
Sergiy Byelozyorovaaf2cc02018-09-24 18:02:28 +00003197 if not options.private and not options.no_autocc:
Aaron Gabled1052492017-05-15 15:05:34 -07003198 cc = self.GetCCList().split(',')
3199 else:
3200 cc = []
tandrii88189772016-09-29 04:29:57 -07003201 if options.cc:
3202 cc.extend(options.cc)
3203 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003204 if change_desc.get_cced():
3205 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003206
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003207 if self.GetIssue():
3208 # GetIssue() is not set in case of non-squash uploads according to tests.
3209 # TODO(agable): non-squash uploads in git cl should be removed.
3210 gerrit_util.AddReviewers(
3211 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003212 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003213 reviewers, cc,
3214 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003215
Aaron Gablefd238082017-06-07 13:42:34 -07003216 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003217 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3218 score = 1
3219 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3220 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3221 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003222 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003223 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003224 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003225 msg='Self-approving for TBR',
3226 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003227
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003228 return 0
3229
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003230 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3231 change_desc):
3232 """Computes parent of the generated commit to be uploaded to Gerrit.
3233
3234 Returns revision or a ref name.
3235 """
3236 if custom_cl_base:
3237 # Try to avoid creating additional unintended CLs when uploading, unless
3238 # user wants to take this risk.
3239 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3240 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3241 local_ref_of_target_remote])
3242 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003243 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003244 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3245 'If you proceed with upload, more than 1 CL may be created by '
3246 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3247 'If you are certain that specified base `%s` has already been '
3248 'uploaded to Gerrit as another CL, you may proceed.\n' %
3249 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3250 if not force:
3251 confirm_or_exit(
3252 'Do you take responsibility for cleaning up potential mess '
3253 'resulting from proceeding with upload?',
3254 action='upload')
3255 return custom_cl_base
3256
Aaron Gablef97e33d2017-03-30 15:44:27 -07003257 if remote != '.':
3258 return self.GetCommonAncestorWithUpstream()
3259
3260 # If our upstream branch is local, we base our squashed commit on its
3261 # squashed version.
3262 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3263
Aaron Gablef97e33d2017-03-30 15:44:27 -07003264 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003265 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003266
3267 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003268 # TODO(tandrii): consider checking parent change in Gerrit and using its
3269 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3270 # the tree hash of the parent branch. The upside is less likely bogus
3271 # requests to reupload parent change just because it's uploadhash is
3272 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003273 parent = RunGit(['config',
3274 'branch.%s.gerritsquashhash' % upstream_branch_name],
3275 error_ok=True).strip()
3276 # Verify that the upstream branch has been uploaded too, otherwise
3277 # Gerrit will create additional CLs when uploading.
3278 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3279 RunGitSilent(['rev-parse', parent + ':'])):
3280 DieWithError(
3281 '\nUpload upstream branch %s first.\n'
3282 'It is likely that this branch has been rebased since its last '
3283 'upload, so you just need to upload it again.\n'
3284 '(If you uploaded it with --no-squash, then branch dependencies '
3285 'are not supported, and you should reupload with --squash.)'
3286 % upstream_branch_name,
3287 change_desc)
3288 return parent
3289
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003290 def _AddChangeIdToCommitMessage(self, options, args):
3291 """Re-commits using the current message, assumes the commit hook is in
3292 place.
3293 """
3294 log_desc = options.message or CreateDescriptionFromLog(args)
3295 git_command = ['commit', '--amend', '-m', log_desc]
3296 RunGit(git_command)
3297 new_log_desc = CreateDescriptionFromLog(args)
3298 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003299 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003300 return new_log_desc
3301 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003302 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003303
Ravi Mistry31e7d562018-04-02 12:53:57 -04003304 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3305 """Sets labels on the change based on the provided flags."""
3306 labels = {}
3307 notify = None;
3308 if enable_auto_submit:
3309 labels['Auto-Submit'] = 1
3310 if use_commit_queue:
3311 labels['Commit-Queue'] = 2
3312 elif cq_dry_run:
3313 labels['Commit-Queue'] = 1
3314 notify = False
3315 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003316 gerrit_util.SetReview(
3317 self._GetGerritHost(),
3318 self._GerritChangeIdentifier(),
3319 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003320
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003321 def SetCQState(self, new_state):
3322 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003323 vote_map = {
3324 _CQState.NONE: 0,
3325 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003326 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003327 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003328 labels = {'Commit-Queue': vote_map[new_state]}
3329 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003330 gerrit_util.SetReview(
3331 self._GetGerritHost(), self._GerritChangeIdentifier(),
3332 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003333
tandriie113dfd2016-10-11 10:20:12 -07003334 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003335 try:
3336 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003337 except GerritChangeNotExists:
3338 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003339
3340 if data['status'] in ('ABANDONED', 'MERGED'):
3341 return 'CL %s is closed' % self.GetIssue()
3342
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003343 def GetTryJobProperties(self, patchset=None):
3344 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003345 data = self._GetChangeDetail(['ALL_REVISIONS'])
3346 patchset = int(patchset or self.GetPatchset())
3347 assert patchset
3348 revision_data = None # Pylint wants it to be defined.
3349 for revision_data in data['revisions'].itervalues():
3350 if int(revision_data['_number']) == patchset:
3351 break
3352 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003353 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003354 (patchset, self.GetIssue()))
3355 return {
3356 'patch_issue': self.GetIssue(),
3357 'patch_set': patchset or self.GetPatchset(),
3358 'patch_project': data['project'],
3359 'patch_storage': 'gerrit',
3360 'patch_ref': revision_data['fetch']['http']['ref'],
3361 'patch_repository_url': revision_data['fetch']['http']['url'],
3362 'patch_gerrit_url': self.GetCodereviewServer(),
3363 }
tandriie113dfd2016-10-11 10:20:12 -07003364
tandriide281ae2016-10-12 06:02:30 -07003365 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003366 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003367
Edward Lemur707d70b2018-02-07 00:50:14 +01003368 def GetReviewers(self):
3369 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3370 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3371
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003372
3373_CODEREVIEW_IMPLEMENTATIONS = {
3374 'rietveld': _RietveldChangelistImpl,
3375 'gerrit': _GerritChangelistImpl,
3376}
3377
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003378
iannuccie53c9352016-08-17 14:40:40 -07003379def _add_codereview_issue_select_options(parser, extra=""):
3380 _add_codereview_select_options(parser)
3381
3382 text = ('Operate on this issue number instead of the current branch\'s '
3383 'implicit issue.')
3384 if extra:
3385 text += ' '+extra
3386 parser.add_option('-i', '--issue', type=int, help=text)
3387
3388
3389def _process_codereview_issue_select_options(parser, options):
3390 _process_codereview_select_options(parser, options)
3391 if options.issue is not None and not options.forced_codereview:
3392 parser.error('--issue must be specified with either --rietveld or --gerrit')
3393
3394
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003395def _add_codereview_select_options(parser):
3396 """Appends --gerrit and --rietveld options to force specific codereview."""
3397 parser.codereview_group = optparse.OptionGroup(
3398 parser, 'EXPERIMENTAL! Codereview override options')
3399 parser.add_option_group(parser.codereview_group)
3400 parser.codereview_group.add_option(
3401 '--gerrit', action='store_true',
3402 help='Force the use of Gerrit for codereview')
3403 parser.codereview_group.add_option(
3404 '--rietveld', action='store_true',
3405 help='Force the use of Rietveld for codereview')
3406
3407
3408def _process_codereview_select_options(parser, options):
3409 if options.gerrit and options.rietveld:
3410 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3411 options.forced_codereview = None
3412 if options.gerrit:
3413 options.forced_codereview = 'gerrit'
3414 elif options.rietveld:
3415 options.forced_codereview = 'rietveld'
3416
3417
tandriif9aefb72016-07-01 09:06:51 -07003418def _get_bug_line_values(default_project, bugs):
3419 """Given default_project and comma separated list of bugs, yields bug line
3420 values.
3421
3422 Each bug can be either:
3423 * a number, which is combined with default_project
3424 * string, which is left as is.
3425
3426 This function may produce more than one line, because bugdroid expects one
3427 project per line.
3428
3429 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3430 ['v8:123', 'chromium:789']
3431 """
3432 default_bugs = []
3433 others = []
3434 for bug in bugs.split(','):
3435 bug = bug.strip()
3436 if bug:
3437 try:
3438 default_bugs.append(int(bug))
3439 except ValueError:
3440 others.append(bug)
3441
3442 if default_bugs:
3443 default_bugs = ','.join(map(str, default_bugs))
3444 if default_project:
3445 yield '%s:%s' % (default_project, default_bugs)
3446 else:
3447 yield default_bugs
3448 for other in sorted(others):
3449 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3450 yield other
3451
3452
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003453class ChangeDescription(object):
3454 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003455 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003456 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003457 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003458 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003459 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3460 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3461 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3462 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003463
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003464 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003465 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003466
agable@chromium.org42c20792013-09-12 17:34:49 +00003467 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003468 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003469 return '\n'.join(self._description_lines)
3470
3471 def set_description(self, desc):
3472 if isinstance(desc, basestring):
3473 lines = desc.splitlines()
3474 else:
3475 lines = [line.rstrip() for line in desc]
3476 while lines and not lines[0]:
3477 lines.pop(0)
3478 while lines and not lines[-1]:
3479 lines.pop(-1)
3480 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003481
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003482 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3483 """Rewrites the R=/TBR= line(s) as a single line each.
3484
3485 Args:
3486 reviewers (list(str)) - list of additional emails to use for reviewers.
3487 tbrs (list(str)) - list of additional emails to use for TBRs.
3488 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3489 the change that are missing OWNER coverage. If this is not None, you
3490 must also pass a value for `change`.
3491 change (Change) - The Change that should be used for OWNERS lookups.
3492 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003493 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003494 assert isinstance(tbrs, list), tbrs
3495
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003496 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003497 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003498
3499 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003500 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003501
3502 reviewers = set(reviewers)
3503 tbrs = set(tbrs)
3504 LOOKUP = {
3505 'TBR': tbrs,
3506 'R': reviewers,
3507 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003508
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003509 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003510 regexp = re.compile(self.R_LINE)
3511 matches = [regexp.match(line) for line in self._description_lines]
3512 new_desc = [l for i, l in enumerate(self._description_lines)
3513 if not matches[i]]
3514 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003515
agable@chromium.org42c20792013-09-12 17:34:49 +00003516 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003517
3518 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003519 for match in matches:
3520 if not match:
3521 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003522 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3523
3524 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003525 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003526 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003527 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003528 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003529 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003530 LOOKUP[add_owners_to].update(
3531 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003532
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003533 # If any folks ended up in both groups, remove them from tbrs.
3534 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003535
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003536 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3537 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003538
3539 # Put the new lines in the description where the old first R= line was.
3540 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3541 if 0 <= line_loc < len(self._description_lines):
3542 if new_tbr_line:
3543 self._description_lines.insert(line_loc, new_tbr_line)
3544 if new_r_line:
3545 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003546 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003547 if new_r_line:
3548 self.append_footer(new_r_line)
3549 if new_tbr_line:
3550 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003551
Aaron Gable3a16ed12017-03-23 10:51:55 -07003552 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003553 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003554 self.set_description([
3555 '# Enter a description of the change.',
3556 '# This will be displayed on the codereview site.',
3557 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003558 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003559 '--------------------',
3560 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003561
agable@chromium.org42c20792013-09-12 17:34:49 +00003562 regexp = re.compile(self.BUG_LINE)
3563 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003564 prefix = settings.GetBugPrefix()
3565 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003566 if git_footer:
3567 self.append_footer('Bug: %s' % ', '.join(values))
3568 else:
3569 for value in values:
3570 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003571
agable@chromium.org42c20792013-09-12 17:34:49 +00003572 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003573 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003574 if not content:
3575 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003576 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003577
Bruce Dawson2377b012018-01-11 16:46:49 -08003578 # Strip off comments and default inserted "Bug:" line.
3579 clean_lines = [line.rstrip() for line in lines if not
3580 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003581 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003582 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003583 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003584
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003585 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003586 """Adds a footer line to the description.
3587
3588 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3589 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3590 that Gerrit footers are always at the end.
3591 """
3592 parsed_footer_line = git_footers.parse_footer(line)
3593 if parsed_footer_line:
3594 # Line is a gerrit footer in the form: Footer-Key: any value.
3595 # Thus, must be appended observing Gerrit footer rules.
3596 self.set_description(
3597 git_footers.add_footer(self.description,
3598 key=parsed_footer_line[0],
3599 value=parsed_footer_line[1]))
3600 return
3601
3602 if not self._description_lines:
3603 self._description_lines.append(line)
3604 return
3605
3606 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3607 if gerrit_footers:
3608 # git_footers.split_footers ensures that there is an empty line before
3609 # actual (gerrit) footers, if any. We have to keep it that way.
3610 assert top_lines and top_lines[-1] == ''
3611 top_lines, separator = top_lines[:-1], top_lines[-1:]
3612 else:
3613 separator = [] # No need for separator if there are no gerrit_footers.
3614
3615 prev_line = top_lines[-1] if top_lines else ''
3616 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3617 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3618 top_lines.append('')
3619 top_lines.append(line)
3620 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003621
tandrii99a72f22016-08-17 14:33:24 -07003622 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003623 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003624 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003625 reviewers = [match.group(2).strip()
3626 for match in matches
3627 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003628 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003629
bradnelsond975b302016-10-23 12:20:23 -07003630 def get_cced(self):
3631 """Retrieves the list of reviewers."""
3632 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3633 cced = [match.group(2).strip() for match in matches if match]
3634 return cleanup_list(cced)
3635
Nodir Turakulov23b82142017-11-16 11:04:25 -08003636 def get_hash_tags(self):
3637 """Extracts and sanitizes a list of Gerrit hashtags."""
3638 subject = (self._description_lines or ('',))[0]
3639 subject = re.sub(
3640 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3641
3642 tags = []
3643 start = 0
3644 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3645 while True:
3646 m = bracket_exp.match(subject, start)
3647 if not m:
3648 break
3649 tags.append(self.sanitize_hash_tag(m.group(1)))
3650 start = m.end()
3651
3652 if not tags:
3653 # Try "Tag: " prefix.
3654 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3655 if m:
3656 tags.append(self.sanitize_hash_tag(m.group(1)))
3657 return tags
3658
3659 @classmethod
3660 def sanitize_hash_tag(cls, tag):
3661 """Returns a sanitized Gerrit hash tag.
3662
3663 A sanitized hashtag can be used as a git push refspec parameter value.
3664 """
3665 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3666
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003667 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3668 """Updates this commit description given the parent.
3669
3670 This is essentially what Gnumbd used to do.
3671 Consult https://goo.gl/WMmpDe for more details.
3672 """
3673 assert parent_msg # No, orphan branch creation isn't supported.
3674 assert parent_hash
3675 assert dest_ref
3676 parent_footer_map = git_footers.parse_footers(parent_msg)
3677 # This will also happily parse svn-position, which GnumbD is no longer
3678 # supporting. While we'd generate correct footers, the verifier plugin
3679 # installed in Gerrit will block such commit (ie git push below will fail).
3680 parent_position = git_footers.get_position(parent_footer_map)
3681
3682 # Cherry-picks may have last line obscuring their prior footers,
3683 # from git_footers perspective. This is also what Gnumbd did.
3684 cp_line = None
3685 if (self._description_lines and
3686 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3687 cp_line = self._description_lines.pop()
3688
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003689 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003690
3691 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3692 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003693 for i, line in enumerate(footer_lines):
3694 k, v = git_footers.parse_footer(line) or (None, None)
3695 if k and k.startswith('Cr-'):
3696 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003697
3698 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003699 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003700 if parent_position[0] == dest_ref:
3701 # Same branch as parent.
3702 number = int(parent_position[1]) + 1
3703 else:
3704 number = 1 # New branch, and extra lineage.
3705 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3706 int(parent_position[1])))
3707
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003708 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3709 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003710
3711 self._description_lines = top_lines
3712 if cp_line:
3713 self._description_lines.append(cp_line)
3714 if self._description_lines[-1] != '':
3715 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003716 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003717
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003718
Aaron Gablea1bab272017-04-11 16:38:18 -07003719def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003720 """Retrieves the reviewers that approved a CL from the issue properties with
3721 messages.
3722
3723 Note that the list may contain reviewers that are not committer, thus are not
3724 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003725
3726 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003727 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003728 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003729 return sorted(
3730 set(
3731 message['sender']
3732 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003733 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003734 )
3735 )
3736
3737
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003738def FindCodereviewSettingsFile(filename='codereview.settings'):
3739 """Finds the given file starting in the cwd and going up.
3740
3741 Only looks up to the top of the repository unless an
3742 'inherit-review-settings-ok' file exists in the root of the repository.
3743 """
3744 inherit_ok_file = 'inherit-review-settings-ok'
3745 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003746 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003747 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3748 root = '/'
3749 while True:
3750 if filename in os.listdir(cwd):
3751 if os.path.isfile(os.path.join(cwd, filename)):
3752 return open(os.path.join(cwd, filename))
3753 if cwd == root:
3754 break
3755 cwd = os.path.dirname(cwd)
3756
3757
3758def LoadCodereviewSettingsFromFile(fileobj):
3759 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003760 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003761
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003762 def SetProperty(name, setting, unset_error_ok=False):
3763 fullname = 'rietveld.' + name
3764 if setting in keyvals:
3765 RunGit(['config', fullname, keyvals[setting]])
3766 else:
3767 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3768
tandrii48df5812016-10-17 03:55:37 -07003769 if not keyvals.get('GERRIT_HOST', False):
3770 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003771 # Only server setting is required. Other settings can be absent.
3772 # In that case, we ignore errors raised during option deletion attempt.
3773 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003774 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003775 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3776 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003777 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003778 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3779 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003780 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003781 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3782 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003783
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003784 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003785 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003786
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003787 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003788 RunGit(['config', 'gerrit.squash-uploads',
3789 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003790
tandrii@chromium.org28253532016-04-14 13:46:56 +00003791 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003792 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003793 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3794
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003795 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003796 # should be of the form
3797 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3798 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003799 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3800 keyvals['ORIGIN_URL_CONFIG']])
3801
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003802
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003803def urlretrieve(source, destination):
3804 """urllib is broken for SSL connections via a proxy therefore we
3805 can't use urllib.urlretrieve()."""
3806 with open(destination, 'w') as f:
3807 f.write(urllib2.urlopen(source).read())
3808
3809
ukai@chromium.org712d6102013-11-27 00:52:58 +00003810def hasSheBang(fname):
3811 """Checks fname is a #! script."""
3812 with open(fname) as f:
3813 return f.read(2).startswith('#!')
3814
3815
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003816# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3817def DownloadHooks(*args, **kwargs):
3818 pass
3819
3820
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003821def DownloadGerritHook(force):
3822 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003823
3824 Args:
3825 force: True to update hooks. False to install hooks if not present.
3826 """
3827 if not settings.GetIsGerrit():
3828 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003829 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003830 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3831 if not os.access(dst, os.X_OK):
3832 if os.path.exists(dst):
3833 if not force:
3834 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003835 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003836 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003837 if not hasSheBang(dst):
3838 DieWithError('Not a script: %s\n'
3839 'You need to download from\n%s\n'
3840 'into .git/hooks/commit-msg and '
3841 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003842 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3843 except Exception:
3844 if os.path.exists(dst):
3845 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003846 DieWithError('\nFailed to download hooks.\n'
3847 'You need to download from\n%s\n'
3848 'into .git/hooks/commit-msg and '
3849 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003850
3851
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003852def GetRietveldCodereviewSettingsInteractively():
3853 """Prompt the user for settings."""
3854 server = settings.GetDefaultServerUrl(error_ok=True)
3855 prompt = 'Rietveld server (host[:port])'
3856 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3857 newserver = ask_for_data(prompt + ':')
3858 if not server and not newserver:
3859 newserver = DEFAULT_SERVER
3860 if newserver:
3861 newserver = gclient_utils.UpgradeToHttps(newserver)
3862 if newserver != server:
3863 RunGit(['config', 'rietveld.server', newserver])
3864
3865 def SetProperty(initial, caption, name, is_url):
3866 prompt = caption
3867 if initial:
3868 prompt += ' ("x" to clear) [%s]' % initial
3869 new_val = ask_for_data(prompt + ':')
3870 if new_val == 'x':
3871 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3872 elif new_val:
3873 if is_url:
3874 new_val = gclient_utils.UpgradeToHttps(new_val)
3875 if new_val != initial:
3876 RunGit(['config', 'rietveld.' + name, new_val])
3877
3878 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3879 SetProperty(settings.GetDefaultPrivateFlag(),
3880 'Private flag (rietveld only)', 'private', False)
3881 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3882 'tree-status-url', False)
3883 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3884 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3885 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3886 'run-post-upload-hook', False)
3887
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003888
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003889class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003890 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003891
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003892 _GOOGLESOURCE = 'googlesource.com'
3893
3894 def __init__(self):
3895 # Cached list of [host, identity, source], where source is either
3896 # .gitcookies or .netrc.
3897 self._all_hosts = None
3898
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003899 def ensure_configured_gitcookies(self):
3900 """Runs checks and suggests fixes to make git use .gitcookies from default
3901 path."""
3902 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3903 configured_path = RunGitSilent(
3904 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003905 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003906 if configured_path:
3907 self._ensure_default_gitcookies_path(configured_path, default)
3908 else:
3909 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003910
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003911 @staticmethod
3912 def _ensure_default_gitcookies_path(configured_path, default_path):
3913 assert configured_path
3914 if configured_path == default_path:
3915 print('git is already configured to use your .gitcookies from %s' %
3916 configured_path)
3917 return
3918
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003919 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003920 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3921 (configured_path, default_path))
3922
3923 if not os.path.exists(configured_path):
3924 print('However, your configured .gitcookies file is missing.')
3925 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3926 action='reconfigure')
3927 RunGit(['config', '--global', 'http.cookiefile', default_path])
3928 return
3929
3930 if os.path.exists(default_path):
3931 print('WARNING: default .gitcookies file already exists %s' %
3932 default_path)
3933 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3934 default_path)
3935
3936 confirm_or_exit('Move existing .gitcookies to default location?',
3937 action='move')
3938 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003939 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003940 print('Moved and reconfigured git to use .gitcookies from %s' %
3941 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003942
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003943 @staticmethod
3944 def _configure_gitcookies_path(default_path):
3945 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3946 if os.path.exists(netrc_path):
3947 print('You seem to be using outdated .netrc for git credentials: %s' %
3948 netrc_path)
3949 print('This tool will guide you through setting up recommended '
3950 '.gitcookies store for git credentials.\n'
3951 '\n'
3952 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3953 ' git config --global --unset http.cookiefile\n'
3954 ' mv %s %s.backup\n\n' % (default_path, default_path))
3955 confirm_or_exit(action='setup .gitcookies')
3956 RunGit(['config', '--global', 'http.cookiefile', default_path])
3957 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003958
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003959 def get_hosts_with_creds(self, include_netrc=False):
3960 if self._all_hosts is None:
3961 a = gerrit_util.CookiesAuthenticator()
3962 self._all_hosts = [
3963 (h, u, s)
3964 for h, u, s in itertools.chain(
3965 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3966 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3967 )
3968 if h.endswith(self._GOOGLESOURCE)
3969 ]
3970
3971 if include_netrc:
3972 return self._all_hosts
3973 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3974
3975 def print_current_creds(self, include_netrc=False):
3976 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3977 if not hosts:
3978 print('No Git/Gerrit credentials found')
3979 return
3980 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3981 header = [('Host', 'User', 'Which file'),
3982 ['=' * l for l in lengths]]
3983 for row in (header + hosts):
3984 print('\t'.join((('%%+%ds' % l) % s)
3985 for l, s in zip(lengths, row)))
3986
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003987 @staticmethod
3988 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003989 """Parses identity "git-<username>.domain" into <username> and domain."""
3990 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003991 # distinguishable from sub-domains. But we do know typical domains:
3992 if identity.endswith('.chromium.org'):
3993 domain = 'chromium.org'
3994 username = identity[:-len('.chromium.org')]
3995 else:
3996 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003997 if username.startswith('git-'):
3998 username = username[len('git-'):]
3999 return username, domain
4000
4001 def _get_usernames_of_domain(self, domain):
4002 """Returns list of usernames referenced by .gitcookies in a given domain."""
4003 identities_by_domain = {}
4004 for _, identity, _ in self.get_hosts_with_creds():
4005 username, domain = self._parse_identity(identity)
4006 identities_by_domain.setdefault(domain, []).append(username)
4007 return identities_by_domain.get(domain)
4008
4009 def _canonical_git_googlesource_host(self, host):
4010 """Normalizes Gerrit hosts (with '-review') to Git host."""
4011 assert host.endswith(self._GOOGLESOURCE)
4012 # Prefix doesn't include '.' at the end.
4013 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
4014 if prefix.endswith('-review'):
4015 prefix = prefix[:-len('-review')]
4016 return prefix + '.' + self._GOOGLESOURCE
4017
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004018 def _canonical_gerrit_googlesource_host(self, host):
4019 git_host = self._canonical_git_googlesource_host(host)
4020 prefix = git_host.split('.', 1)[0]
4021 return prefix + '-review.' + self._GOOGLESOURCE
4022
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004023 def _get_counterpart_host(self, host):
4024 assert host.endswith(self._GOOGLESOURCE)
4025 git = self._canonical_git_googlesource_host(host)
4026 gerrit = self._canonical_gerrit_googlesource_host(git)
4027 return git if gerrit == host else gerrit
4028
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004029 def has_generic_host(self):
4030 """Returns whether generic .googlesource.com has been configured.
4031
4032 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
4033 """
4034 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
4035 if host == '.' + self._GOOGLESOURCE:
4036 return True
4037 return False
4038
4039 def _get_git_gerrit_identity_pairs(self):
4040 """Returns map from canonic host to pair of identities (Git, Gerrit).
4041
4042 One of identities might be None, meaning not configured.
4043 """
4044 host_to_identity_pairs = {}
4045 for host, identity, _ in self.get_hosts_with_creds():
4046 canonical = self._canonical_git_googlesource_host(host)
4047 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
4048 idx = 0 if canonical == host else 1
4049 pair[idx] = identity
4050 return host_to_identity_pairs
4051
4052 def get_partially_configured_hosts(self):
4053 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004054 (host if i1 else self._canonical_gerrit_googlesource_host(host))
4055 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
4056 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004057
4058 def get_conflicting_hosts(self):
4059 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004060 host
4061 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004062 if None not in (i1, i2) and i1 != i2)
4063
4064 def get_duplicated_hosts(self):
4065 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
4066 return set(host for host, count in counters.iteritems() if count > 1)
4067
4068 _EXPECTED_HOST_IDENTITY_DOMAINS = {
4069 'chromium.googlesource.com': 'chromium.org',
4070 'chrome-internal.googlesource.com': 'google.com',
4071 }
4072
4073 def get_hosts_with_wrong_identities(self):
4074 """Finds hosts which **likely** reference wrong identities.
4075
4076 Note: skips hosts which have conflicting identities for Git and Gerrit.
4077 """
4078 hosts = set()
4079 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
4080 pair = self._get_git_gerrit_identity_pairs().get(host)
4081 if pair and pair[0] == pair[1]:
4082 _, domain = self._parse_identity(pair[0])
4083 if domain != expected:
4084 hosts.add(host)
4085 return hosts
4086
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004087 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004088 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004089 hosts = sorted(hosts)
4090 assert hosts
4091 if extra_column_func is None:
4092 extras = [''] * len(hosts)
4093 else:
4094 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004095 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
4096 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004097 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004098 lines.append(tmpl % he)
4099 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004100
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004101 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004102 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004103 yield ('.googlesource.com wildcard record detected',
4104 ['Chrome Infrastructure team recommends to list full host names '
4105 'explicitly.'],
4106 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004107
4108 dups = self.get_duplicated_hosts()
4109 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004110 yield ('The following hosts were defined twice',
4111 self._format_hosts(dups),
4112 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004113
4114 partial = self.get_partially_configured_hosts()
4115 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004116 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4117 'These hosts are missing',
4118 self._format_hosts(partial, lambda host: 'but %s defined' %
4119 self._get_counterpart_host(host)),
4120 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004121
4122 conflicting = self.get_conflicting_hosts()
4123 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004124 yield ('The following Git hosts have differing credentials from their '
4125 'Gerrit counterparts',
4126 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4127 tuple(self._get_git_gerrit_identity_pairs()[host])),
4128 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004129
4130 wrong = self.get_hosts_with_wrong_identities()
4131 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004132 yield ('These hosts likely use wrong identity',
4133 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4134 (self._get_git_gerrit_identity_pairs()[host][0],
4135 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4136 wrong)
4137
4138 def find_and_report_problems(self):
4139 """Returns True if there was at least one problem, else False."""
4140 found = False
4141 bad_hosts = set()
4142 for title, sublines, hosts in self._find_problems():
4143 if not found:
4144 found = True
4145 print('\n\n.gitcookies problem report:\n')
4146 bad_hosts.update(hosts or [])
4147 print(' %s%s' % (title , (':' if sublines else '')))
4148 if sublines:
4149 print()
4150 print(' %s' % '\n '.join(sublines))
4151 print()
4152
4153 if bad_hosts:
4154 assert found
4155 print(' You can manually remove corresponding lines in your %s file and '
4156 'visit the following URLs with correct account to generate '
4157 'correct credential lines:\n' %
4158 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4159 print(' %s' % '\n '.join(sorted(set(
4160 gerrit_util.CookiesAuthenticator().get_new_password_url(
4161 self._canonical_git_googlesource_host(host))
4162 for host in bad_hosts
4163 ))))
4164 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004165
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004166
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004167@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004168def CMDcreds_check(parser, args):
4169 """Checks credentials and suggests changes."""
4170 _, _ = parser.parse_args(args)
4171
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004172 # Code below checks .gitcookies. Abort if using something else.
4173 authn = gerrit_util.Authenticator.get()
4174 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
4175 if isinstance(authn, gerrit_util.GceAuthenticator):
4176 DieWithError(
4177 'This command is not designed for GCE, are you on a bot?\n'
4178 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
4179 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004180 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004181 'This command is not designed for bot environment. It checks '
4182 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004183
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004184 checker = _GitCookiesChecker()
4185 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004186
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004187 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004188 checker.print_current_creds(include_netrc=True)
4189
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004190 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004191 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004192 return 0
4193 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004194
4195
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004196@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004197@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004198def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004199 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004200
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004201 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004202 # TODO(tandrii): remove this once we switch to Gerrit.
4203 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004204 parser.add_option('--activate-update', action='store_true',
4205 help='activate auto-updating [rietveld] section in '
4206 '.git/config')
4207 parser.add_option('--deactivate-update', action='store_true',
4208 help='deactivate auto-updating [rietveld] section in '
4209 '.git/config')
4210 options, args = parser.parse_args(args)
4211
4212 if options.deactivate_update:
4213 RunGit(['config', 'rietveld.autoupdate', 'false'])
4214 return
4215
4216 if options.activate_update:
4217 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4218 return
4219
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004220 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004221 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004222 return 0
4223
4224 url = args[0]
4225 if not url.endswith('codereview.settings'):
4226 url = os.path.join(url, 'codereview.settings')
4227
4228 # Load code review settings and download hooks (if available).
4229 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4230 return 0
4231
4232
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004233@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004234def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004235 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004236 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4237 branch = ShortBranchName(branchref)
4238 _, args = parser.parse_args(args)
4239 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004240 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004241 return RunGit(['config', 'branch.%s.base-url' % branch],
4242 error_ok=False).strip()
4243 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004244 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004245 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4246 error_ok=False).strip()
4247
4248
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004249def color_for_status(status):
4250 """Maps a Changelist status to color, for CMDstatus and other tools."""
4251 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004252 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004253 'waiting': Fore.BLUE,
4254 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004255 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004256 'lgtm': Fore.GREEN,
4257 'commit': Fore.MAGENTA,
4258 'closed': Fore.CYAN,
4259 'error': Fore.WHITE,
4260 }.get(status, Fore.WHITE)
4261
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004262
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004263def get_cl_statuses(changes, fine_grained, max_processes=None):
4264 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004265
4266 If fine_grained is true, this will fetch CL statuses from the server.
4267 Otherwise, simply indicate if there's a matching url for the given branches.
4268
4269 If max_processes is specified, it is used as the maximum number of processes
4270 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4271 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004272
4273 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004274 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004275 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004276 upload.verbosity = 0
4277
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004278 if not changes:
4279 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004280
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004281 if not fine_grained:
4282 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004283 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004284 for cl in changes:
4285 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004286 return
4287
4288 # First, sort out authentication issues.
4289 logging.debug('ensuring credentials exist')
4290 for cl in changes:
4291 cl.EnsureAuthenticated(force=False, refresh=True)
4292
4293 def fetch(cl):
4294 try:
4295 return (cl, cl.GetStatus())
4296 except:
4297 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004298 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004299 raise
4300
4301 threads_count = len(changes)
4302 if max_processes:
4303 threads_count = max(1, min(threads_count, max_processes))
4304 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4305
4306 pool = ThreadPool(threads_count)
4307 fetched_cls = set()
4308 try:
4309 it = pool.imap_unordered(fetch, changes).__iter__()
4310 while True:
4311 try:
4312 cl, status = it.next(timeout=5)
4313 except multiprocessing.TimeoutError:
4314 break
4315 fetched_cls.add(cl)
4316 yield cl, status
4317 finally:
4318 pool.close()
4319
4320 # Add any branches that failed to fetch.
4321 for cl in set(changes) - fetched_cls:
4322 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004323
rmistry@google.com2dd99862015-06-22 12:22:18 +00004324
4325def upload_branch_deps(cl, args):
4326 """Uploads CLs of local branches that are dependents of the current branch.
4327
4328 If the local branch dependency tree looks like:
4329 test1 -> test2.1 -> test3.1
4330 -> test3.2
4331 -> test2.2 -> test3.3
4332
4333 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4334 run on the dependent branches in this order:
4335 test2.1, test3.1, test3.2, test2.2, test3.3
4336
4337 Note: This function does not rebase your local dependent branches. Use it when
4338 you make a change to the parent branch that will not conflict with its
4339 dependent branches, and you would like their dependencies updated in
4340 Rietveld.
4341 """
4342 if git_common.is_dirty_git_tree('upload-branch-deps'):
4343 return 1
4344
4345 root_branch = cl.GetBranch()
4346 if root_branch is None:
4347 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4348 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004349 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004350 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4351 'patchset dependencies without an uploaded CL.')
4352
4353 branches = RunGit(['for-each-ref',
4354 '--format=%(refname:short) %(upstream:short)',
4355 'refs/heads'])
4356 if not branches:
4357 print('No local branches found.')
4358 return 0
4359
4360 # Create a dictionary of all local branches to the branches that are dependent
4361 # on it.
4362 tracked_to_dependents = collections.defaultdict(list)
4363 for b in branches.splitlines():
4364 tokens = b.split()
4365 if len(tokens) == 2:
4366 branch_name, tracked = tokens
4367 tracked_to_dependents[tracked].append(branch_name)
4368
vapiera7fbd5a2016-06-16 09:17:49 -07004369 print()
4370 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004371 dependents = []
4372 def traverse_dependents_preorder(branch, padding=''):
4373 dependents_to_process = tracked_to_dependents.get(branch, [])
4374 padding += ' '
4375 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004376 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004377 dependents.append(dependent)
4378 traverse_dependents_preorder(dependent, padding)
4379 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004380 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004381
4382 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004383 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004384 return 0
4385
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004386 confirm_or_exit('This command will checkout all dependent branches and run '
4387 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004388
andybons@chromium.org962f9462016-02-03 20:00:42 +00004389 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004390 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004391 args.extend(['-t', 'Updated patchset dependency'])
4392
rmistry@google.com2dd99862015-06-22 12:22:18 +00004393 # Record all dependents that failed to upload.
4394 failures = {}
4395 # Go through all dependents, checkout the branch and upload.
4396 try:
4397 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004398 print()
4399 print('--------------------------------------')
4400 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004401 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004402 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004403 try:
4404 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004405 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004406 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004407 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004408 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004409 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004410 finally:
4411 # Swap back to the original root branch.
4412 RunGit(['checkout', '-q', root_branch])
4413
vapiera7fbd5a2016-06-16 09:17:49 -07004414 print()
4415 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004416 for dependent_branch in dependents:
4417 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004418 print(' %s : %s' % (dependent_branch, upload_status))
4419 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004420
4421 return 0
4422
4423
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004424@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004425def CMDarchive(parser, args):
4426 """Archives and deletes branches associated with closed changelists."""
4427 parser.add_option(
4428 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004429 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004430 parser.add_option(
4431 '-f', '--force', action='store_true',
4432 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004433 parser.add_option(
4434 '-d', '--dry-run', action='store_true',
4435 help='Skip the branch tagging and removal steps.')
4436 parser.add_option(
4437 '-t', '--notags', action='store_true',
4438 help='Do not tag archived branches. '
4439 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004440
4441 auth.add_auth_options(parser)
4442 options, args = parser.parse_args(args)
4443 if args:
4444 parser.error('Unsupported args: %s' % ' '.join(args))
4445 auth_config = auth.extract_auth_config_from_options(options)
4446
4447 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4448 if not branches:
4449 return 0
4450
vapiera7fbd5a2016-06-16 09:17:49 -07004451 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004452 changes = [Changelist(branchref=b, auth_config=auth_config)
4453 for b in branches.splitlines()]
4454 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4455 statuses = get_cl_statuses(changes,
4456 fine_grained=True,
4457 max_processes=options.maxjobs)
4458 proposal = [(cl.GetBranch(),
4459 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4460 for cl, status in statuses
4461 if status == 'closed']
4462 proposal.sort()
4463
4464 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004465 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004466 return 0
4467
4468 current_branch = GetCurrentBranch()
4469
vapiera7fbd5a2016-06-16 09:17:49 -07004470 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004471 if options.notags:
4472 for next_item in proposal:
4473 print(' ' + next_item[0])
4474 else:
4475 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4476 for next_item in proposal:
4477 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004478
kmarshall9249e012016-08-23 12:02:16 -07004479 # Quit now on precondition failure or if instructed by the user, either
4480 # via an interactive prompt or by command line flags.
4481 if options.dry_run:
4482 print('\nNo changes were made (dry run).\n')
4483 return 0
4484 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004485 print('You are currently on a branch \'%s\' which is associated with a '
4486 'closed codereview issue, so archive cannot proceed. Please '
4487 'checkout another branch and run this command again.' %
4488 current_branch)
4489 return 1
kmarshall9249e012016-08-23 12:02:16 -07004490 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004491 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4492 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004493 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004494 return 1
4495
4496 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004497 if not options.notags:
4498 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004499 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004500
vapiera7fbd5a2016-06-16 09:17:49 -07004501 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004502
4503 return 0
4504
4505
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004506@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004507def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004508 """Show status of changelists.
4509
4510 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004511 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004512 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004513 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004514 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004515 - Magenta in the commit queue
4516 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004517 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004518
4519 Also see 'git cl comments'.
4520 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004521 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004522 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004523 parser.add_option('-f', '--fast', action='store_true',
4524 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004525 parser.add_option(
4526 '-j', '--maxjobs', action='store', type=int,
4527 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004528
4529 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004530 _add_codereview_issue_select_options(
4531 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004532 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004533 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004534 if args:
4535 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004536 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537
iannuccie53c9352016-08-17 14:40:40 -07004538 if options.issue is not None and not options.field:
4539 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004540
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004541 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004542 cl = Changelist(auth_config=auth_config, issue=options.issue,
4543 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004544 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004545 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004546 elif options.field == 'id':
4547 issueid = cl.GetIssue()
4548 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004549 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004550 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004551 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004552 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004553 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004554 elif options.field == 'status':
4555 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004556 elif options.field == 'url':
4557 url = cl.GetIssueURL()
4558 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004559 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004560 return 0
4561
4562 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4563 if not branches:
4564 print('No local branch found.')
4565 return 0
4566
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004567 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004568 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004569 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004570 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004571 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004572 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004573 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004574
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004575 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004576 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4577 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4578 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004579 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004580 c, status = output.next()
4581 branch_statuses[c.GetBranch()] = status
4582 status = branch_statuses.pop(branch)
4583 url = cl.GetIssueURL()
4584 if url and (not status or status == 'error'):
4585 # The issue probably doesn't exist anymore.
4586 url += ' (broken)'
4587
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004588 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004589 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004590 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004591 color = ''
4592 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004593 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004594 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004595 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004596 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004597
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004598
4599 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004600 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004601 print('Current branch: %s' % branch)
4602 for cl in changes:
4603 if cl.GetBranch() == branch:
4604 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004605 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004606 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004607 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004608 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004609 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004610 print('Issue description:')
4611 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004612 return 0
4613
4614
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004615def colorize_CMDstatus_doc():
4616 """To be called once in main() to add colors to git cl status help."""
4617 colors = [i for i in dir(Fore) if i[0].isupper()]
4618
4619 def colorize_line(line):
4620 for color in colors:
4621 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004622 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004623 indent = len(line) - len(line.lstrip(' ')) + 1
4624 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4625 return line
4626
4627 lines = CMDstatus.__doc__.splitlines()
4628 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4629
4630
phajdan.jre328cf92016-08-22 04:12:17 -07004631def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004632 if path == '-':
4633 json.dump(contents, sys.stdout)
4634 else:
4635 with open(path, 'w') as f:
4636 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004637
4638
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004639@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004640@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004641def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004642 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004643
4644 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004645 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004646 parser.add_option('-r', '--reverse', action='store_true',
4647 help='Lookup the branch(es) for the specified issues. If '
4648 'no issues are specified, all branches with mapped '
4649 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004650 parser.add_option('--json',
4651 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004652 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004653 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004654 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004655
dnj@chromium.org406c4402015-03-03 17:22:28 +00004656 if options.reverse:
4657 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004658 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004659 # Reverse issue lookup.
4660 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004661
4662 git_config = {}
4663 for config in RunGit(['config', '--get-regexp',
4664 r'branch\..*issue']).splitlines():
4665 name, _space, val = config.partition(' ')
4666 git_config[name] = val
4667
dnj@chromium.org406c4402015-03-03 17:22:28 +00004668 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004669 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4670 config_key = _git_branch_config_key(ShortBranchName(branch),
4671 cls.IssueConfigKey())
4672 issue = git_config.get(config_key)
4673 if issue:
4674 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004675 if not args:
4676 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004677 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004678 for issue in args:
4679 if not issue:
4680 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004681 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004682 print('Branch for issue number %s: %s' % (
4683 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004684 if options.json:
4685 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004686 return 0
4687
4688 if len(args) > 0:
4689 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4690 if not issue.valid:
4691 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4692 'or no argument to list it.\n'
4693 'Maybe you want to run git cl status?')
4694 cl = Changelist(codereview=issue.codereview)
4695 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004696 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004697 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004698 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4699 if options.json:
4700 write_json(options.json, {
4701 'issue': cl.GetIssue(),
4702 'issue_url': cl.GetIssueURL(),
4703 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004704 return 0
4705
4706
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004707@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004708def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004709 """Shows or posts review comments for any changelist."""
4710 parser.add_option('-a', '--add-comment', dest='comment',
4711 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004712 parser.add_option('-i', '--issue', dest='issue',
4713 help='review issue id (defaults to current issue). '
4714 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004715 parser.add_option('-m', '--machine-readable', dest='readable',
4716 action='store_false', default=True,
4717 help='output comments in a format compatible with '
4718 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004719 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004720 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004721 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004722 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004723 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004724 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004725 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004726
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004727 issue = None
4728 if options.issue:
4729 try:
4730 issue = int(options.issue)
4731 except ValueError:
4732 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004733 if not options.forced_codereview:
4734 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004735
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004736 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004737 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004738 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004739
4740 if options.comment:
4741 cl.AddComment(options.comment)
4742 return 0
4743
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004744 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4745 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004746 for comment in summary:
4747 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004748 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004749 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004750 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004751 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004752 color = Fore.MAGENTA
4753 else:
4754 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004755 print('\n%s%s %s%s\n%s' % (
4756 color,
4757 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4758 comment.sender,
4759 Fore.RESET,
4760 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4761
smut@google.comc85ac942015-09-15 16:34:43 +00004762 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004763 def pre_serialize(c):
4764 dct = c.__dict__.copy()
4765 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4766 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004767 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004768 return 0
4769
4770
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004771@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004772@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004773def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004774 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004775 parser.add_option('-d', '--display', action='store_true',
4776 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004777 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004778 help='New description to set for this issue (- for stdin, '
4779 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004780 parser.add_option('-f', '--force', action='store_true',
4781 help='Delete any unpublished Gerrit edits for this issue '
4782 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004783
4784 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004785 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004786 options, args = parser.parse_args(args)
4787 _process_codereview_select_options(parser, options)
4788
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004789 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004790 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004791 target_issue_arg = ParseIssueNumberArgument(args[0],
4792 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004793 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004794 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004795
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004796 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004797
martiniss6eda05f2016-06-30 10:18:35 -07004798 kwargs = {
4799 'auth_config': auth_config,
4800 'codereview': options.forced_codereview,
4801 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004802 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004803 if target_issue_arg:
4804 kwargs['issue'] = target_issue_arg.issue
4805 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004806 if target_issue_arg.codereview and not options.forced_codereview:
4807 detected_codereview_from_url = True
4808 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004809
4810 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004811 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004812 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004813 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004814
4815 if detected_codereview_from_url:
4816 logging.info('canonical issue/change URL: %s (type: %s)\n',
4817 cl.GetIssueURL(), target_issue_arg.codereview)
4818
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004819 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004820
smut@google.com34fb6b12015-07-13 20:03:26 +00004821 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004822 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004823 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004824
4825 if options.new_description:
4826 text = options.new_description
4827 if text == '-':
4828 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004829 elif text == '+':
4830 base_branch = cl.GetCommonAncestorWithUpstream()
4831 change = cl.GetChange(base_branch, None, local_description=True)
4832 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004833
4834 description.set_description(text)
4835 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004836 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004837
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004838 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004839 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004840 return 0
4841
4842
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004843def CreateDescriptionFromLog(args):
4844 """Pulls out the commit log to use as a base for the CL description."""
4845 log_args = []
4846 if len(args) == 1 and not args[0].endswith('.'):
4847 log_args = [args[0] + '..']
4848 elif len(args) == 1 and args[0].endswith('...'):
4849 log_args = [args[0][:-1]]
4850 elif len(args) == 2:
4851 log_args = [args[0] + '..' + args[1]]
4852 else:
4853 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004854 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004855
4856
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004857@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004858def CMDlint(parser, args):
4859 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004860 parser.add_option('--filter', action='append', metavar='-x,+y',
4861 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004862 auth.add_auth_options(parser)
4863 options, args = parser.parse_args(args)
4864 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004865
4866 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004867 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004868 try:
4869 import cpplint
4870 import cpplint_chromium
4871 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004872 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004873 return 1
4874
4875 # Change the current working directory before calling lint so that it
4876 # shows the correct base.
4877 previous_cwd = os.getcwd()
4878 os.chdir(settings.GetRoot())
4879 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004880 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004881 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4882 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004883 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004884 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004885 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004886
4887 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004888 command = args + files
4889 if options.filter:
4890 command = ['--filter=' + ','.join(options.filter)] + command
4891 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004892
4893 white_regex = re.compile(settings.GetLintRegex())
4894 black_regex = re.compile(settings.GetLintIgnoreRegex())
4895 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4896 for filename in filenames:
4897 if white_regex.match(filename):
4898 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004899 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004900 else:
4901 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4902 extra_check_functions)
4903 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004904 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004905 finally:
4906 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004907 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004908 if cpplint._cpplint_state.error_count != 0:
4909 return 1
4910 return 0
4911
4912
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004913@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004914def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004915 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004916 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004917 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004918 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004919 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004920 parser.add_option('--all', action='store_true',
4921 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004922 parser.add_option('--parallel', action='store_true',
4923 help='Run all tests specified by input_api.RunTests in all '
4924 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004925 auth.add_auth_options(parser)
4926 options, args = parser.parse_args(args)
4927 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004928
sbc@chromium.org71437c02015-04-09 19:29:40 +00004929 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004930 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004931 return 1
4932
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004933 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004934 if args:
4935 base_branch = args[0]
4936 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004937 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004938 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004939
Aaron Gable8076c282017-11-29 14:39:41 -08004940 if options.all:
4941 base_change = cl.GetChange(base_branch, None)
4942 files = [('M', f) for f in base_change.AllFiles()]
4943 change = presubmit_support.GitChange(
4944 base_change.Name(),
4945 base_change.FullDescriptionText(),
4946 base_change.RepositoryRoot(),
4947 files,
4948 base_change.issue,
4949 base_change.patchset,
4950 base_change.author_email,
4951 base_change._upstream)
4952 else:
4953 change = cl.GetChange(base_branch, None)
4954
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004955 cl.RunHook(
4956 committing=not options.upload,
4957 may_prompt=False,
4958 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004959 change=change,
4960 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004961 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004962
4963
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004964def GenerateGerritChangeId(message):
4965 """Returns Ixxxxxx...xxx change id.
4966
4967 Works the same way as
4968 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4969 but can be called on demand on all platforms.
4970
4971 The basic idea is to generate git hash of a state of the tree, original commit
4972 message, author/committer info and timestamps.
4973 """
4974 lines = []
4975 tree_hash = RunGitSilent(['write-tree'])
4976 lines.append('tree %s' % tree_hash.strip())
4977 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4978 if code == 0:
4979 lines.append('parent %s' % parent.strip())
4980 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4981 lines.append('author %s' % author.strip())
4982 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4983 lines.append('committer %s' % committer.strip())
4984 lines.append('')
4985 # Note: Gerrit's commit-hook actually cleans message of some lines and
4986 # whitespace. This code is not doing this, but it clearly won't decrease
4987 # entropy.
4988 lines.append(message)
4989 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4990 stdin='\n'.join(lines))
4991 return 'I%s' % change_hash.strip()
4992
4993
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004994def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004995 """Computes the remote branch ref to use for the CL.
4996
4997 Args:
4998 remote (str): The git remote for the CL.
4999 remote_branch (str): The git remote branch for the CL.
5000 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00005001 """
5002 if not (remote and remote_branch):
5003 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005004
wittman@chromium.org455dc922015-01-26 20:15:50 +00005005 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005006 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00005007 # refs, which are then translated into the remote full symbolic refs
5008 # below.
5009 if '/' not in target_branch:
5010 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
5011 else:
5012 prefix_replacements = (
5013 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
5014 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
5015 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
5016 )
5017 match = None
5018 for regex, replacement in prefix_replacements:
5019 match = re.search(regex, target_branch)
5020 if match:
5021 remote_branch = target_branch.replace(match.group(0), replacement)
5022 break
5023 if not match:
5024 # This is a branch path but not one we recognize; use as-is.
5025 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00005026 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
5027 # Handle the refs that need to land in different refs.
5028 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005029
wittman@chromium.org455dc922015-01-26 20:15:50 +00005030 # Create the true path to the remote branch.
5031 # Does the following translation:
5032 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
5033 # * refs/remotes/origin/master -> refs/heads/master
5034 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
5035 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
5036 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
5037 elif remote_branch.startswith('refs/remotes/%s/' % remote):
5038 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
5039 'refs/heads/')
5040 elif remote_branch.startswith('refs/remotes/branch-heads'):
5041 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01005042
wittman@chromium.org455dc922015-01-26 20:15:50 +00005043 return remote_branch
5044
5045
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005046def cleanup_list(l):
5047 """Fixes a list so that comma separated items are put as individual items.
5048
5049 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
5050 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
5051 """
5052 items = sum((i.split(',') for i in l), [])
5053 stripped_items = (i.strip() for i in items)
5054 return sorted(filter(None, stripped_items))
5055
5056
Aaron Gable4db38df2017-11-03 14:59:07 -07005057@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005058@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005059def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00005060 """Uploads the current changelist to codereview.
5061
5062 Can skip dependency patchset uploads for a branch by running:
5063 git config branch.branch_name.skip-deps-uploads True
5064 To unset run:
5065 git config --unset branch.branch_name.skip-deps-uploads
5066 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02005067
5068 If the name of the checked out branch starts with "bug-" or "fix-" followed by
5069 a bug number, this bug number is automatically populated in the CL
5070 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005071
5072 If subject contains text in square brackets or has "<text>: " prefix, such
5073 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
5074 [git-cl] add support for hashtags
5075 Foo bar: implement foo
5076 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00005077 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00005078 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5079 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00005080 parser.add_option('--bypass-watchlists', action='store_true',
5081 dest='bypass_watchlists',
5082 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07005083 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00005084 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005085 parser.add_option('--message', '-m', dest='message',
5086 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07005087 parser.add_option('-b', '--bug',
5088 help='pre-populate the bug number(s) for this issue. '
5089 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07005090 parser.add_option('--message-file', dest='message_file',
5091 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005092 parser.add_option('--title', '-t', dest='title',
5093 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005094 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005095 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00005096 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005097 parser.add_option('--tbrs',
5098 action='append', default=[],
5099 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005100 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005101 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00005102 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005103 parser.add_option('--hashtag', dest='hashtags',
5104 action='append', default=[],
5105 help=('Gerrit hashtag for new CL; '
5106 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00005107 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08005108 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005109 parser.add_option('--emulate_svn_auto_props',
5110 '--emulate-svn-auto-props',
5111 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00005112 dest="emulate_svn_auto_props",
5113 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00005114 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07005115 help='tell the commit queue to commit this patchset; '
5116 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00005117 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005118 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00005119 metavar='TARGET',
5120 help='Apply CL to remote ref TARGET. ' +
5121 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005122 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005123 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00005124 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005125 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005126 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005127 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005128 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5129 const='TBR', help='add a set of OWNERS to TBR')
5130 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5131 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005132 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5133 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005134 help='Send the patchset to do a CQ dry run right after '
5135 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005136 parser.add_option('--dependencies', action='store_true',
5137 help='Uploads CLs of all the local branches that depend on '
5138 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04005139 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5140 help='Sends your change to the CQ after an approval. Only '
5141 'works on repos that have the Auto-Submit label '
5142 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04005143 parser.add_option('--parallel', action='store_true',
5144 help='Run all tests specified by input_api.RunTests in all '
5145 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005146
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005147 parser.add_option('--no-autocc', action='store_true',
5148 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005149 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005150 help='Set the review private. This implies --no-autocc.')
5151
5152 # TODO: remove Rietveld flags
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005153 parser.add_option('--email', default=None,
5154 help='email address to use to connect to Rietveld')
5155
rmistry@google.com2dd99862015-06-22 12:22:18 +00005156 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005157 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005158 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005159 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005160 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005161 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005162
sbc@chromium.org71437c02015-04-09 19:29:40 +00005163 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005164 return 1
5165
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005166 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005167 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005168 options.cc = cleanup_list(options.cc)
5169
tandriib80458a2016-06-23 12:20:07 -07005170 if options.message_file:
5171 if options.message:
5172 parser.error('only one of --message and --message-file allowed.')
5173 options.message = gclient_utils.FileRead(options.message_file)
5174 options.message_file = None
5175
tandrii4d0545a2016-07-06 03:56:49 -07005176 if options.cq_dry_run and options.use_commit_queue:
5177 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5178
Aaron Gableedbc4132017-09-11 13:22:28 -07005179 if options.use_commit_queue:
5180 options.send_mail = True
5181
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005182 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5183 settings.GetIsGerrit()
5184
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005185 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005186 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005187
5188
Francois Dorayd42c6812017-05-30 15:10:20 -04005189@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005190@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005191def CMDsplit(parser, args):
5192 """Splits a branch into smaller branches and uploads CLs.
5193
5194 Creates a branch and uploads a CL for each group of files modified in the
5195 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005196 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005197 the shared OWNERS file.
5198 """
5199 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005200 help="A text file containing a CL description in which "
5201 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005202 parser.add_option("-c", "--comment", dest="comment_file",
5203 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005204 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5205 default=False,
5206 help="List the files and reviewers for each CL that would "
5207 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00005208 parser.add_option("--cq-dry-run", action='store_true',
5209 help="If set, will do a cq dry run for each uploaded CL. "
5210 "Please be careful when doing this; more than ~10 CLs "
5211 "has the potential to overload our build "
5212 "infrastructure. Try to upload these not during high "
5213 "load times (usually 11-3 Mountain View time). Email "
5214 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005215 options, _ = parser.parse_args(args)
5216
5217 if not options.description_file:
5218 parser.error('No --description flag specified.')
5219
5220 def WrappedCMDupload(args):
5221 return CMDupload(OptionParser(), args)
5222
5223 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00005224 Changelist, WrappedCMDupload, options.dry_run,
5225 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005226
5227
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005228@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005229@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005230def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005231 """DEPRECATED: Used to commit the current changelist via git-svn."""
5232 message = ('git-cl no longer supports committing to SVN repositories via '
5233 'git-svn. You probably want to use `git cl land` instead.')
5234 print(message)
5235 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005236
5237
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005238# Two special branches used by git cl land.
5239MERGE_BRANCH = 'git-cl-commit'
5240CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5241
5242
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005243@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005244@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005245def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005246 """Commits the current changelist via git.
5247
5248 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5249 upstream and closes the issue automatically and atomically.
5250
5251 Otherwise (in case of Rietveld):
5252 Squashes branch into a single commit.
5253 Updates commit message with metadata (e.g. pointer to review).
5254 Pushes the code upstream.
5255 Updates review and closes.
5256 """
5257 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5258 help='bypass upload presubmit hook')
5259 parser.add_option('-m', dest='message',
5260 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005261 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005262 help="force yes to questions (don't prompt)")
5263 parser.add_option('-c', dest='contributor',
5264 help="external contributor for patch (appended to " +
5265 "description and used as author for git). Should be " +
5266 "formatted as 'First Last <email@example.com>'")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005267 parser.add_option('--parallel', action='store_true',
5268 help='Run all tests specified by input_api.RunTests in all '
5269 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005270 auth.add_auth_options(parser)
5271 (options, args) = parser.parse_args(args)
5272 auth_config = auth.extract_auth_config_from_options(options)
5273
5274 cl = Changelist(auth_config=auth_config)
5275
Robert Iannucci2e73d432018-03-14 01:10:47 -07005276 if not cl.IsGerrit():
5277 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005278
Robert Iannucci2e73d432018-03-14 01:10:47 -07005279 if options.message:
5280 # This could be implemented, but it requires sending a new patch to
5281 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5282 # Besides, Gerrit has the ability to change the commit message on submit
5283 # automatically, thus there is no need to support this option (so far?).
5284 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005285 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005286 parser.error(
5287 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5288 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5289 'the contributor\'s "name <email>". If you can\'t upload such a '
5290 'commit for review, contact your repository admin and request'
5291 '"Forge-Author" permission.')
5292 if not cl.GetIssue():
5293 DieWithError('You must upload the change first to Gerrit.\n'
5294 ' If you would rather have `git cl land` upload '
5295 'automatically for you, see http://crbug.com/642759')
5296 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005297 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005298
5299
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005300def PushToGitWithAutoRebase(remote, branch, original_description,
5301 git_numberer_enabled, max_attempts=3):
5302 """Pushes current HEAD commit on top of remote's branch.
5303
5304 Attempts to fetch and autorebase on push failures.
5305 Adds git number footers on the fly.
5306
5307 Returns integer code from last command.
5308 """
5309 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5310 code = 0
5311 attempts_left = max_attempts
5312 while attempts_left:
5313 attempts_left -= 1
5314 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5315
5316 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5317 # If fetch fails, retry.
5318 print('Fetching %s/%s...' % (remote, branch))
5319 code, out = RunGitWithCode(
5320 ['retry', 'fetch', remote,
5321 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5322 if code:
5323 print('Fetch failed with exit code %d.' % code)
5324 print(out.strip())
5325 continue
5326
5327 print('Cherry-picking commit on top of latest %s' % branch)
5328 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5329 suppress_stderr=True)
5330 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5331 code, out = RunGitWithCode(['cherry-pick', cherry])
5332 if code:
5333 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5334 'the following files have merge conflicts:' %
5335 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005336 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5337 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005338 print('Please rebase your patch and try again.')
5339 RunGitWithCode(['cherry-pick', '--abort'])
5340 break
5341
5342 commit_desc = ChangeDescription(original_description)
5343 if git_numberer_enabled:
5344 logging.debug('Adding git number footers')
5345 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5346 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5347 branch)
5348 # Ensure timestamps are monotonically increasing.
5349 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5350 _get_committer_timestamp('HEAD'))
5351 _git_amend_head(commit_desc.description, timestamp)
5352
5353 code, out = RunGitWithCode(
5354 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5355 print(out)
5356 if code == 0:
5357 break
5358 if IsFatalPushFailure(out):
5359 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005360 'user.email are correct and you have push access to the repo.\n'
5361 'Hint: run command below to diangose common Git/Gerrit credential '
5362 'problems:\n'
5363 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005364 break
5365 return code
5366
5367
5368def IsFatalPushFailure(push_stdout):
5369 """True if retrying push won't help."""
5370 return '(prohibited by Gerrit)' in push_stdout
5371
5372
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005373@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005374@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005375def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005376 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005377 parser.add_option('-b', dest='newbranch',
5378 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005379 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005380 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005381 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005382 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005383 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005384 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005385 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005386 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005387 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005388 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005389
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005390
5391 group = optparse.OptionGroup(
5392 parser,
5393 'Options for continuing work on the current issue uploaded from a '
5394 'different clone (e.g. different machine). Must be used independently '
5395 'from the other options. No issue number should be specified, and the '
5396 'branch must have an issue number associated with it')
5397 group.add_option('--reapply', action='store_true', dest='reapply',
5398 help='Reset the branch and reapply the issue.\n'
5399 'CAUTION: This will undo any local changes in this '
5400 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005401
5402 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005403 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005404 parser.add_option_group(group)
5405
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005406 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005407 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005408 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005409 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005410 auth_config = auth.extract_auth_config_from_options(options)
5411
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005412 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005413 if options.newbranch:
5414 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005415 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005416 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005417
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005418 cl = Changelist(auth_config=auth_config,
5419 codereview=options.forced_codereview)
5420 if not cl.GetIssue():
5421 parser.error('current branch must have an associated issue')
5422
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005423 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005424 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005425 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005426
5427 RunGit(['reset', '--hard', upstream])
5428 if options.pull:
5429 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005430
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005431 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5432 options.directory)
5433
5434 if len(args) != 1 or not args[0]:
5435 parser.error('Must specify issue number or url')
5436
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005437 target_issue_arg = ParseIssueNumberArgument(args[0],
5438 options.forced_codereview)
5439 if not target_issue_arg.valid:
5440 parser.error('invalid codereview url or CL id')
5441
5442 cl_kwargs = {
5443 'auth_config': auth_config,
5444 'codereview_host': target_issue_arg.hostname,
5445 'codereview': options.forced_codereview,
5446 }
5447 detected_codereview_from_url = False
5448 if target_issue_arg.codereview and not options.forced_codereview:
5449 detected_codereview_from_url = True
5450 cl_kwargs['codereview'] = target_issue_arg.codereview
5451 cl_kwargs['issue'] = target_issue_arg.issue
5452
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005453 # We don't want uncommitted changes mixed up with the patch.
5454 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005455 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005456
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005457 if options.newbranch:
5458 if options.force:
5459 RunGit(['branch', '-D', options.newbranch],
5460 stderr=subprocess2.PIPE, error_ok=True)
5461 RunGit(['new-branch', options.newbranch])
5462
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005463 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005464
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005465 if cl.IsGerrit():
5466 if options.reject:
5467 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005468 if options.directory:
5469 parser.error('--directory is not supported with Gerrit codereview.')
5470
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005471 if detected_codereview_from_url:
5472 print('canonical issue/change URL: %s (type: %s)\n' %
5473 (cl.GetIssueURL(), target_issue_arg.codereview))
5474
5475 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005476 options.nocommit, options.directory,
5477 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005478
5479
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005480def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005481 """Fetches the tree status and returns either 'open', 'closed',
5482 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005483 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005484 if url:
5485 status = urllib2.urlopen(url).read().lower()
5486 if status.find('closed') != -1 or status == '0':
5487 return 'closed'
5488 elif status.find('open') != -1 or status == '1':
5489 return 'open'
5490 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005491 return 'unset'
5492
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005493
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005494def GetTreeStatusReason():
5495 """Fetches the tree status from a json url and returns the message
5496 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005497 url = settings.GetTreeStatusUrl()
5498 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005499 connection = urllib2.urlopen(json_url)
5500 status = json.loads(connection.read())
5501 connection.close()
5502 return status['message']
5503
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005504
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005505@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005506def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005507 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005508 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005509 status = GetTreeStatus()
5510 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005511 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005512 return 2
5513
vapiera7fbd5a2016-06-16 09:17:49 -07005514 print('The tree is %s' % status)
5515 print()
5516 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005517 if status != 'open':
5518 return 1
5519 return 0
5520
5521
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005522@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005523def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005524 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005525 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005526 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005527 '-b', '--bot', action='append',
5528 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5529 'times to specify multiple builders. ex: '
5530 '"-b win_rel -b win_layout". See '
5531 'the try server waterfall for the builders name and the tests '
5532 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005533 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005534 '-B', '--bucket', default='',
5535 help=('Buildbucket bucket to send the try requests.'))
5536 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005537 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005538 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005539 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005540 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005541 help='Revision to use for the try job; default: the revision will '
5542 'be determined by the try recipe that builder runs, which usually '
5543 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005544 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005545 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005546 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005547 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005548 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005549 '--category', default='git_cl_try', help='Specify custom build category.')
5550 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005551 '--project',
5552 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005553 'in recipe to determine to which repository or directory to '
5554 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005555 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005556 '-p', '--property', dest='properties', action='append', default=[],
5557 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005558 'key2=value2 etc. The value will be treated as '
5559 'json if decodable, or as string otherwise. '
5560 'NOTE: using this may make your try job not usable for CQ, '
5561 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005562 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005563 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5564 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005565 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005566 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005567 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005568 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005569 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005570 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005571
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005572 if options.master and options.master.startswith('luci.'):
5573 parser.error(
5574 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005575 # Make sure that all properties are prop=value pairs.
5576 bad_params = [x for x in options.properties if '=' not in x]
5577 if bad_params:
5578 parser.error('Got properties with missing "=": %s' % bad_params)
5579
maruel@chromium.org15192402012-09-06 12:38:29 +00005580 if args:
5581 parser.error('Unknown arguments: %s' % args)
5582
Koji Ishii31c14782018-01-08 17:17:33 +09005583 cl = Changelist(auth_config=auth_config, issue=options.issue,
5584 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005585 if not cl.GetIssue():
5586 parser.error('Need to upload first')
5587
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005588 if cl.IsGerrit():
5589 # HACK: warm up Gerrit change detail cache to save on RPCs.
5590 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5591
tandriie113dfd2016-10-11 10:20:12 -07005592 error_message = cl.CannotTriggerTryJobReason()
5593 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005594 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005595
borenet6c0efe62016-10-19 08:13:29 -07005596 if options.bucket and options.master:
5597 parser.error('Only one of --bucket and --master may be used.')
5598
qyearsley1fdfcb62016-10-24 13:22:03 -07005599 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005600
qyearsleydd49f942016-10-28 11:57:22 -07005601 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5602 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005603 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005604 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005605 print('git cl try with no bots now defaults to CQ dry run.')
5606 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5607 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005608
borenet6c0efe62016-10-19 08:13:29 -07005609 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005610 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005611 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005612 'of bot requires an initial job from a parent (usually a builder). '
5613 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005614 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005615 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005616
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005617 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005618 # TODO(tandrii): Checking local patchset against remote patchset is only
5619 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5620 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005621 print('Warning: Codereview server has newer patchsets (%s) than most '
5622 'recent upload from local checkout (%s). Did a previous upload '
5623 'fail?\n'
5624 'By default, git cl try uses the latest patchset from '
5625 'codereview, continuing to use patchset %s.\n' %
5626 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005627
tandrii568043b2016-10-11 07:49:18 -07005628 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005629 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005630 except BuildbucketResponseException as ex:
5631 print('ERROR: %s' % ex)
5632 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005633 return 0
5634
5635
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005636@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005637def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005638 """Prints info about try jobs associated with current CL."""
5639 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005640 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005641 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005642 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005643 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005644 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005645 '--color', action='store_true', default=setup_color.IS_TTY,
5646 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005647 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005648 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5649 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005650 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005651 '--json', help=('Path of JSON output file to write try job results to,'
5652 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005653 parser.add_option_group(group)
5654 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005655 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005656 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005657 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005658 if args:
5659 parser.error('Unrecognized args: %s' % ' '.join(args))
5660
5661 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005662 cl = Changelist(
5663 issue=options.issue, codereview=options.forced_codereview,
5664 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005665 if not cl.GetIssue():
5666 parser.error('Need to upload first')
5667
tandrii221ab252016-10-06 08:12:04 -07005668 patchset = options.patchset
5669 if not patchset:
5670 patchset = cl.GetMostRecentPatchset()
5671 if not patchset:
5672 parser.error('Codereview doesn\'t know about issue %s. '
5673 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005674 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005675 cl.GetIssue())
5676
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005677 # TODO(tandrii): Checking local patchset against remote patchset is only
5678 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5679 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005680 print('Warning: Codereview server has newer patchsets (%s) than most '
5681 'recent upload from local checkout (%s). Did a previous upload '
5682 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005683 'By default, git cl try-results uses the latest patchset from '
5684 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005685 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005686 try:
tandrii221ab252016-10-06 08:12:04 -07005687 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005688 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005689 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005690 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005691 if options.json:
5692 write_try_results_json(options.json, jobs)
5693 else:
5694 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005695 return 0
5696
5697
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005698@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005699@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005700def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005701 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005702 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005703 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005704 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005705
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005706 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005707 if args:
5708 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005709 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005710 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005711 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005712 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005713
5714 # Clear configured merge-base, if there is one.
5715 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005716 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005717 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005718 return 0
5719
5720
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005721@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005722def CMDweb(parser, args):
5723 """Opens the current CL in the web browser."""
5724 _, args = parser.parse_args(args)
5725 if args:
5726 parser.error('Unrecognized args: %s' % ' '.join(args))
5727
5728 issue_url = Changelist().GetIssueURL()
5729 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005730 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005731 return 1
5732
5733 webbrowser.open(issue_url)
5734 return 0
5735
5736
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005737@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005738def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005739 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005740 parser.add_option('-d', '--dry-run', action='store_true',
5741 help='trigger in dry run mode')
5742 parser.add_option('-c', '--clear', action='store_true',
5743 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005744 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005745 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005746 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005747 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005748 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005749 if args:
5750 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005751 if options.dry_run and options.clear:
5752 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5753
iannuccie53c9352016-08-17 14:40:40 -07005754 cl = Changelist(auth_config=auth_config, issue=options.issue,
5755 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005756 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005757 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005758 elif options.dry_run:
5759 state = _CQState.DRY_RUN
5760 else:
5761 state = _CQState.COMMIT
5762 if not cl.GetIssue():
5763 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005764 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005765 return 0
5766
5767
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005768@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005769def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005770 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005771 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005772 auth.add_auth_options(parser)
5773 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005774 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005775 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005776 if args:
5777 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005778 cl = Changelist(auth_config=auth_config, issue=options.issue,
5779 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005780 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005781 if not cl.GetIssue():
5782 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005783 cl.CloseIssue()
5784 return 0
5785
5786
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005787@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005788def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005789 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005790 parser.add_option(
5791 '--stat',
5792 action='store_true',
5793 dest='stat',
5794 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005795 auth.add_auth_options(parser)
5796 options, args = parser.parse_args(args)
5797 auth_config = auth.extract_auth_config_from_options(options)
5798 if args:
5799 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005800
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005801 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005802 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005803 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005804 if not issue:
5805 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005806
Aaron Gablea718c3e2017-08-28 17:47:28 -07005807 base = cl._GitGetBranchConfigValue('last-upload-hash')
5808 if not base:
5809 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5810 if not base:
5811 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5812 revision_info = detail['revisions'][detail['current_revision']]
5813 fetch_info = revision_info['fetch']['http']
5814 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5815 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005816
Aaron Gablea718c3e2017-08-28 17:47:28 -07005817 cmd = ['git', 'diff']
5818 if options.stat:
5819 cmd.append('--stat')
5820 cmd.append(base)
5821 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005822
5823 return 0
5824
5825
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005826@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005827def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005828 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005829 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005830 '--ignore-current',
5831 action='store_true',
5832 help='Ignore the CL\'s current reviewers and start from scratch.')
5833 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005834 '--no-color',
5835 action='store_true',
5836 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005837 parser.add_option(
5838 '--batch',
5839 action='store_true',
5840 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005841 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005842 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005843 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005844
5845 author = RunGit(['config', 'user.email']).strip() or None
5846
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005847 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005848
5849 if args:
5850 if len(args) > 1:
5851 parser.error('Unknown args')
5852 base_branch = args[0]
5853 else:
5854 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005855 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005856
5857 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005858 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5859
5860 if options.batch:
5861 db = owners.Database(change.RepositoryRoot(), file, os.path)
5862 print('\n'.join(db.reviewers_for(affected_files, author)))
5863 return 0
5864
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005865 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005866 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005867 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005868 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005869 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005870 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005871 disable_color=options.no_color,
5872 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005873
5874
Aiden Bennerc08566e2018-10-03 17:52:42 +00005875def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005876 """Generates a diff command."""
5877 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005878 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5879
5880 if not allow_prefix:
5881 diff_cmd += ['--no-prefix']
5882
5883 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005884
5885 if args:
5886 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005887 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005888 diff_cmd.append(arg)
5889 else:
5890 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005891
5892 return diff_cmd
5893
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005894
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005895def MatchingFileType(file_name, extensions):
5896 """Returns true if the file name ends with one of the given extensions."""
5897 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005898
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005899
enne@chromium.org555cfe42014-01-29 18:21:39 +00005900@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005901@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005902def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005903 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005904 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005905 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005906 parser.add_option('--full', action='store_true',
5907 help='Reformat the full content of all touched files')
5908 parser.add_option('--dry-run', action='store_true',
5909 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005910 parser.add_option('--python', action='store_true',
5911 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005912 parser.add_option('--js', action='store_true',
5913 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005914 parser.add_option('--diff', action='store_true',
5915 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005916 parser.add_option('--presubmit', action='store_true',
5917 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005918 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005919
Daniel Chengc55eecf2016-12-30 03:11:02 -08005920 # Normalize any remaining args against the current path, so paths relative to
5921 # the current directory are still resolved as expected.
5922 args = [os.path.join(os.getcwd(), arg) for arg in args]
5923
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005924 # git diff generates paths against the root of the repository. Change
5925 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005926 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005927 if rel_base_path:
5928 os.chdir(rel_base_path)
5929
digit@chromium.org29e47272013-05-17 17:01:46 +00005930 # Grab the merge-base commit, i.e. the upstream commit of the current
5931 # branch when it was created or the last time it was rebased. This is
5932 # to cover the case where the user may have called "git fetch origin",
5933 # moving the origin branch to a newer commit, but hasn't rebased yet.
5934 upstream_commit = None
5935 cl = Changelist()
5936 upstream_branch = cl.GetUpstreamBranch()
5937 if upstream_branch:
5938 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5939 upstream_commit = upstream_commit.strip()
5940
5941 if not upstream_commit:
5942 DieWithError('Could not find base commit for this branch. '
5943 'Are you in detached state?')
5944
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005945 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5946 diff_output = RunGit(changed_files_cmd)
5947 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005948 # Filter out files deleted by this CL
5949 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005950
Christopher Lamc5ba6922017-01-24 11:19:14 +11005951 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005952 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005953
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005954 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5955 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5956 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005957 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005958
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005959 top_dir = os.path.normpath(
5960 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5961
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005962 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5963 # formatted. This is used to block during the presubmit.
5964 return_value = 0
5965
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005966 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005967 # Locate the clang-format binary in the checkout
5968 try:
5969 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005970 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005971 DieWithError(e)
5972
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005973 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005974 cmd = [clang_format_tool]
5975 if not opts.dry_run and not opts.diff:
5976 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005977 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005978 if opts.diff:
5979 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005980 else:
5981 env = os.environ.copy()
5982 env['PATH'] = str(os.path.dirname(clang_format_tool))
5983 try:
5984 script = clang_format.FindClangFormatScriptInChromiumTree(
5985 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005986 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005987 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005988
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005989 cmd = [sys.executable, script, '-p0']
5990 if not opts.dry_run and not opts.diff:
5991 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005992
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005993 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5994 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005995
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005996 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5997 if opts.diff:
5998 sys.stdout.write(stdout)
5999 if opts.dry_run and len(stdout) > 0:
6000 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006001
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006002 # Similar code to above, but using yapf on .py files rather than clang-format
6003 # on C/C++ files
Aiden Bennerc08566e2018-10-03 17:52:42 +00006004 if opts.python and python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006005 yapf_tool = gclient_utils.FindExecutable('yapf')
6006 if yapf_tool is None:
6007 DieWithError('yapf not found in PATH')
6008
Aiden Bennerc08566e2018-10-03 17:52:42 +00006009 # If we couldn't find a yapf file we'll default to the chromium style
6010 # specified in depot_tools.
6011 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
6012 chromium_default_yapf_style = os.path.join(depot_tools_path,
6013 YAPF_CONFIG_FILENAME)
6014
6015 # Note: yapf still seems to fix indentation of the entire file
6016 # even if line ranges are specified.
6017 # See https://github.com/google/yapf/issues/499
6018 if not opts.full:
6019 py_line_diffs = _ComputeDiffLineRanges(python_diff_files, upstream_commit)
6020
6021 # Used for caching.
6022 yapf_configs = {}
6023 for f in python_diff_files:
6024 # Find the yapf style config for the current file, defaults to depot
6025 # tools default.
6026 yapf_config = _FindYapfConfigFile(
6027 os.path.abspath(f), yapf_configs, top_dir,
6028 chromium_default_yapf_style)
6029
6030 cmd = [yapf_tool, '--style', yapf_config, f]
6031
6032 has_formattable_lines = False
6033 if not opts.full:
6034 # Only run yapf over changed line ranges.
6035 for diff_start, diff_len in py_line_diffs[f]:
6036 diff_end = diff_start + diff_len - 1
6037 # Yapf errors out if diff_end < diff_start but this
6038 # is a valid line range diff for a removal.
6039 if diff_end >= diff_start:
6040 has_formattable_lines = True
6041 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
6042 # If all line diffs were removals we have nothing to format.
6043 if not has_formattable_lines:
6044 continue
6045
6046 if opts.diff or opts.dry_run:
6047 cmd += ['--diff']
6048 # Will return non-zero exit code if non-empty diff.
6049 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
6050 if opts.diff:
6051 sys.stdout.write(stdout)
6052 elif len(stdout) > 0:
6053 return_value = 2
6054 else:
6055 cmd += ['-i']
6056 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006057
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006058 # Dart's formatter does not have the nice property of only operating on
6059 # modified chunks, so hard code full.
6060 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006061 try:
6062 command = [dart_format.FindDartFmtToolInChromiumTree()]
6063 if not opts.dry_run and not opts.diff:
6064 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006065 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006066
ppi@chromium.org6593d932016-03-03 15:41:15 +00006067 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006068 if opts.dry_run and stdout:
6069 return_value = 2
6070 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07006071 print('Warning: Unable to check Dart code formatting. Dart SDK not '
6072 'found in this checkout. Files in other languages are still '
6073 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006074
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006075 # Format GN build files. Always run on full build files for canonical form.
6076 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006077 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006078 if opts.dry_run or opts.diff:
6079 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006080 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006081 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6082 shell=sys.platform == 'win32',
6083 cwd=top_dir)
6084 if opts.dry_run and gn_ret == 2:
6085 return_value = 2 # Not formatted.
6086 elif opts.diff and gn_ret == 2:
6087 # TODO this should compute and print the actual diff.
6088 print("This change has GN build file diff for " + gn_diff_file)
6089 elif gn_ret != 0:
6090 # For non-dry run cases (and non-2 return values for dry-run), a
6091 # nonzero error code indicates a failure, probably because the file
6092 # doesn't parse.
6093 DieWithError("gn format failed on " + gn_diff_file +
6094 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006095
Ilya Shermane081cbe2017-08-15 17:51:04 -07006096 # Skip the metrics formatting from the global presubmit hook. These files have
6097 # a separate presubmit hook that issues an error if the files need formatting,
6098 # whereas the top-level presubmit script merely issues a warning. Formatting
6099 # these files is somewhat slow, so it's important not to duplicate the work.
6100 if not opts.presubmit:
6101 for xml_dir in GetDirtyMetricsDirs(diff_files):
6102 tool_dir = os.path.join(top_dir, xml_dir)
6103 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
6104 if opts.dry_run or opts.diff:
6105 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07006106 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07006107 if opts.diff:
6108 sys.stdout.write(stdout)
6109 if opts.dry_run and stdout:
6110 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006111
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006112 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006113
Steven Holte2e664bf2017-04-21 13:10:47 -07006114def GetDirtyMetricsDirs(diff_files):
6115 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
6116 metrics_xml_dirs = [
6117 os.path.join('tools', 'metrics', 'actions'),
6118 os.path.join('tools', 'metrics', 'histograms'),
6119 os.path.join('tools', 'metrics', 'rappor'),
6120 os.path.join('tools', 'metrics', 'ukm')]
6121 for xml_dir in metrics_xml_dirs:
6122 if any(file.startswith(xml_dir) for file in xml_diff_files):
6123 yield xml_dir
6124
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006125
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006126@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006127@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006128def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006129 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006130 _, args = parser.parse_args(args)
6131
6132 if len(args) != 1:
6133 parser.print_help()
6134 return 1
6135
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006136 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006137 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006138 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006139
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006140 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006141
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006142 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006143 output = RunGit(['config', '--local', '--get-regexp',
6144 r'branch\..*\.%s' % issueprefix],
6145 error_ok=True)
6146 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006147 if issue == target_issue:
6148 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006149
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006150 branches = []
6151 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006152 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006153 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006154 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006155 return 1
6156 if len(branches) == 1:
6157 RunGit(['checkout', branches[0]])
6158 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006159 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006160 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006161 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006162 which = raw_input('Choose by index: ')
6163 try:
6164 RunGit(['checkout', branches[int(which)]])
6165 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006166 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006167 return 1
6168
6169 return 0
6170
6171
maruel@chromium.org29404b52014-09-08 22:58:00 +00006172def CMDlol(parser, args):
6173 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006174 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006175 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6176 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6177 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006178 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006179 return 0
6180
6181
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006182class OptionParser(optparse.OptionParser):
6183 """Creates the option parse and add --verbose support."""
6184 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006185 optparse.OptionParser.__init__(
6186 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006187 self.add_option(
6188 '-v', '--verbose', action='count', default=0,
6189 help='Use 2 times for more debugging info')
6190
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006191 def parse_args(self, args=None, _values=None):
6192 # Create an optparse.Values object that will store only the actual passed
6193 # options, without the defaults.
6194 actual_options = optparse.Values()
6195 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6196 # Create an optparse.Values object with the default options.
6197 options = optparse.Values(self.get_default_values().__dict__)
6198 # Update it with the options passed by the user.
6199 options._update_careful(actual_options.__dict__)
6200 # Store the options passed by the user in an _actual_options attribute.
6201 # We store only the keys, and not the values, since the values can contain
6202 # arbitrary information, which might be PII.
6203 metrics.collector.add('arguments', actual_options.__dict__.keys())
6204
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006205 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006206 logging.basicConfig(
6207 level=levels[min(options.verbose, len(levels) - 1)],
6208 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6209 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006210 return options, args
6211
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006212
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006213def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006214 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006215 print('\nYour python version %s is unsupported, please upgrade.\n' %
6216 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006217 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006218
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006219 # Reload settings.
6220 global settings
6221 settings = Settings()
6222
Edward Lemurad463c92018-07-25 21:31:23 +00006223 if not metrics.DISABLE_METRICS_COLLECTION:
6224 metrics.collector.add('project_urls', [settings.GetViewVCUrl().strip('/+')])
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006225 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006226 dispatcher = subcommand.CommandDispatcher(__name__)
6227 try:
6228 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006229 except auth.AuthenticationError as e:
6230 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006231 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006232 if e.code != 500:
6233 raise
6234 DieWithError(
6235 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6236 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006237 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006238
6239
6240if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006241 # These affect sys.stdout so do it outside of main() to simplify mocks in
6242 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006243 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006244 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00006245 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00006246 sys.exit(main(sys.argv[1:]))