blob: c06fc87cda9fe67296419f432407f1fde114ce1f [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 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 Shyshkalovcd6a9362016-12-07 12:04:12 +010017import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000018import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000019import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000021import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import optparse
23import os
24import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000025import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080036 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
skobes6468b902016-10-24 08:45:10 -070044import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080067POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
borenet6c0efe62016-10-19 08:13:29 -070078# Buildbucket master name prefix.
79MASTER_PREFIX = 'master.'
80
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000081# Shortcut since it quickly becomes redundant.
82Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000083
maruel@chromium.orgddd59412011-11-30 14:20:38 +000084# Initialized in main()
85settings = None
86
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010087# Used by tests/git_cl_test.py to add extra logging.
88# Inside the weirdly failing test, add this:
89# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
90# And scroll up to see the strack trace printed.
91_IS_BEING_TESTED = False
92
maruel@chromium.orgddd59412011-11-30 14:20:38 +000093
Christopher Lamf732cd52017-01-24 12:40:11 +110094def DieWithError(message, change_desc=None):
95 if change_desc:
96 SaveDescriptionBackup(change_desc)
97
vapiera7fbd5a2016-06-16 09:17:49 -070098 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 sys.exit(1)
100
101
Christopher Lamf732cd52017-01-24 12:40:11 +1100102def SaveDescriptionBackup(change_desc):
103 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
104 print('\nError after CL description prompt -- saving description to %s\n' %
105 backup_path)
106 backup_file = open(backup_path, 'w')
107 backup_file.write(change_desc.description)
108 backup_file.close()
109
110
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000111def GetNoGitPagerEnv():
112 env = os.environ.copy()
113 # 'cat' is a magical git string that disables pagers on all platforms.
114 env['GIT_PAGER'] = 'cat'
115 return env
116
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000117
bsep@chromium.org627d9002016-04-29 00:00:52 +0000118def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000119 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000120 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000121 except subprocess2.CalledProcessError as e:
122 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000123 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000124 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000125 'Command "%s" failed.\n%s' % (
126 ' '.join(args), error_message or e.stdout or ''))
127 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128
129
130def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000131 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000132 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000135def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700137 if suppress_stderr:
138 stderr = subprocess2.VOID
139 else:
140 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000141 try:
tandrii5d48c322016-08-18 16:19:37 -0700142 (out, _), code = subprocess2.communicate(['git'] + args,
143 env=GetNoGitPagerEnv(),
144 stdout=subprocess2.PIPE,
145 stderr=stderr)
146 return code, out
147 except subprocess2.CalledProcessError as e:
148 logging.debug('Failed running %s', args)
149 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000150
151
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000152def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000153 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000154 return RunGitWithCode(args, suppress_stderr=True)[1]
155
156
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000157def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000158 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000159 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000160 return (version.startswith(prefix) and
161 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162
163
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000164def BranchExists(branch):
165 """Return True if specified branch exists."""
166 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
167 suppress_stderr=True)
168 return not code
169
170
tandrii2a16b952016-10-19 07:09:44 -0700171def time_sleep(seconds):
172 # Use this so that it can be mocked in tests without interfering with python
173 # system machinery.
174 import time # Local import to discourage others from importing time globally.
175 return time.sleep(seconds)
176
177
maruel@chromium.org90541732011-04-01 17:54:18 +0000178def ask_for_data(prompt):
179 try:
180 return raw_input(prompt)
181 except KeyboardInterrupt:
182 # Hide the exception.
183 sys.exit(1)
184
185
tandrii5d48c322016-08-18 16:19:37 -0700186def _git_branch_config_key(branch, key):
187 """Helper method to return Git config key for a branch."""
188 assert branch, 'branch name is required to set git config for it'
189 return 'branch.%s.%s' % (branch, key)
190
191
192def _git_get_branch_config_value(key, default=None, value_type=str,
193 branch=False):
194 """Returns git config value of given or current branch if any.
195
196 Returns default in all other cases.
197 """
198 assert value_type in (int, str, bool)
199 if branch is False: # Distinguishing default arg value from None.
200 branch = GetCurrentBranch()
201
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000202 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700203 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000204
tandrii5d48c322016-08-18 16:19:37 -0700205 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700206 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700207 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700208 # git config also has --int, but apparently git config suffers from integer
209 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700210 args.append(_git_branch_config_key(branch, key))
211 code, out = RunGitWithCode(args)
212 if code == 0:
213 value = out.strip()
214 if value_type == int:
215 return int(value)
216 if value_type == bool:
217 return bool(value.lower() == 'true')
218 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000219 return default
220
221
tandrii5d48c322016-08-18 16:19:37 -0700222def _git_set_branch_config_value(key, value, branch=None, **kwargs):
223 """Sets the value or unsets if it's None of a git branch config.
224
225 Valid, though not necessarily existing, branch must be provided,
226 otherwise currently checked out branch is used.
227 """
228 if not branch:
229 branch = GetCurrentBranch()
230 assert branch, 'a branch name OR currently checked out branch is required'
231 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700232 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700233 if value is None:
234 args.append('--unset')
235 elif isinstance(value, bool):
236 args.append('--bool')
237 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700238 else:
tandrii33a46ff2016-08-23 05:53:40 -0700239 # git config also has --int, but apparently git config suffers from integer
240 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700241 value = str(value)
242 args.append(_git_branch_config_key(branch, key))
243 if value is not None:
244 args.append(value)
245 RunGit(args, **kwargs)
246
247
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100248def _get_committer_timestamp(commit):
249 """Returns unix timestamp as integer of a committer in a commit.
250
251 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
252 """
253 # Git also stores timezone offset, but it only affects visual display,
254 # actual point in time is defined by this timestamp only.
255 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
256
257
258def _git_amend_head(message, committer_timestamp):
259 """Amends commit with new message and desired committer_timestamp.
260
261 Sets committer timezone to UTC.
262 """
263 env = os.environ.copy()
264 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
265 return RunGit(['commit', '--amend', '-m', message], env=env)
266
267
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000268def add_git_similarity(parser):
269 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700270 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000271 help='Sets the percentage that a pair of files need to match in order to'
272 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000273 parser.add_option(
274 '--find-copies', action='store_true',
275 help='Allows git to look for copies.')
276 parser.add_option(
277 '--no-find-copies', action='store_false', dest='find_copies',
278 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000279
280 old_parser_args = parser.parse_args
281 def Parse(args):
282 options, args = old_parser_args(args)
283
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000284 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700285 options.similarity = _git_get_branch_config_value(
286 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000287 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000288 print('Note: Saving similarity of %d%% in git config.'
289 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700290 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000291
iannucci@chromium.org79540052012-10-19 23:15:26 +0000292 options.similarity = max(0, min(options.similarity, 100))
293
294 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700295 options.find_copies = _git_get_branch_config_value(
296 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000297 else:
tandrii5d48c322016-08-18 16:19:37 -0700298 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000299
300 print('Using %d%% similarity for rename/copy detection. '
301 'Override with --similarity.' % options.similarity)
302
303 return options, args
304 parser.parse_args = Parse
305
306
machenbach@chromium.org45453142015-09-15 08:45:22 +0000307def _get_properties_from_options(options):
308 properties = dict(x.split('=', 1) for x in options.properties)
309 for key, val in properties.iteritems():
310 try:
311 properties[key] = json.loads(val)
312 except ValueError:
313 pass # If a value couldn't be evaluated, treat it as a string.
314 return properties
315
316
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000317def _prefix_master(master):
318 """Convert user-specified master name to full master name.
319
320 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
321 name, while the developers always use shortened master name
322 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
323 function does the conversion for buildbucket migration.
324 """
borenet6c0efe62016-10-19 08:13:29 -0700325 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000326 return master
borenet6c0efe62016-10-19 08:13:29 -0700327 return '%s%s' % (MASTER_PREFIX, master)
328
329
330def _unprefix_master(bucket):
331 """Convert bucket name to shortened master name.
332
333 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
334 name, while the developers always use shortened master name
335 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
336 function does the conversion for buildbucket migration.
337 """
338 if bucket.startswith(MASTER_PREFIX):
339 return bucket[len(MASTER_PREFIX):]
340 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000341
342
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000343def _buildbucket_retry(operation_name, http, *args, **kwargs):
344 """Retries requests to buildbucket service and returns parsed json content."""
345 try_count = 0
346 while True:
347 response, content = http.request(*args, **kwargs)
348 try:
349 content_json = json.loads(content)
350 except ValueError:
351 content_json = None
352
353 # Buildbucket could return an error even if status==200.
354 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000355 error = content_json.get('error')
356 if error.get('code') == 403:
357 raise BuildbucketResponseException(
358 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000360 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000361 raise BuildbucketResponseException(msg)
362
363 if response.status == 200:
364 if not content_json:
365 raise BuildbucketResponseException(
366 'Buildbucket returns invalid json content: %s.\n'
367 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
368 content)
369 return content_json
370 if response.status < 500 or try_count >= 2:
371 raise httplib2.HttpLib2Error(content)
372
373 # status >= 500 means transient failures.
374 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700375 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000376 try_count += 1
377 assert False, 'unreachable'
378
379
qyearsley1fdfcb62016-10-24 13:22:03 -0700380def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700381 """Returns a dict mapping bucket names to builders and tests,
382 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700383 """
qyearsleydd49f942016-10-28 11:57:22 -0700384 # If no bots are listed, we try to get a set of builders and tests based
385 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700386 if not options.bot:
387 change = changelist.GetChange(
388 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700389 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700390 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700391 change=change,
392 changed_files=change.LocalPaths(),
393 repository_root=settings.GetRoot(),
394 default_presubmit=None,
395 project=None,
396 verbose=options.verbose,
397 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700398 if masters is None:
399 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100400 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700401
qyearsley1fdfcb62016-10-24 13:22:03 -0700402 if options.bucket:
403 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700404 if options.master:
405 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
qyearsleydd49f942016-10-28 11:57:22 -0700407 # If bots are listed but no master or bucket, then we need to find out
408 # the corresponding master for each bot.
409 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
410 if error_message:
411 option_parser.error(
412 'Tryserver master cannot be found because: %s\n'
413 'Please manually specify the tryserver master, e.g. '
414 '"-m tryserver.chromium.linux".' % error_message)
415 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700416
417
qyearsley123a4682016-10-26 09:12:17 -0700418def _get_bucket_map_for_builders(builders):
419 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700420 map_url = 'https://builders-map.appspot.com/'
421 try:
qyearsley123a4682016-10-26 09:12:17 -0700422 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700423 except urllib2.URLError as e:
424 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
425 (map_url, e))
426 except ValueError as e:
427 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700428 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700429 return None, 'Failed to build master map.'
430
qyearsley123a4682016-10-26 09:12:17 -0700431 bucket_map = {}
432 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700433 masters = builders_map.get(builder, [])
434 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700435 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700436 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700437 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700438 (builder, masters))
439 bucket = _prefix_master(masters[0])
440 bucket_map.setdefault(bucket, {})[builder] = []
441
442 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700443
444
borenet6c0efe62016-10-19 08:13:29 -0700445def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700446 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700447 """Sends a request to Buildbucket to trigger try jobs for a changelist.
448
449 Args:
450 auth_config: AuthConfig for Rietveld.
451 changelist: Changelist that the try jobs are associated with.
452 buckets: A nested dict mapping bucket names to builders to tests.
453 options: Command-line options.
454 """
tandriide281ae2016-10-12 06:02:30 -0700455 assert changelist.GetIssue(), 'CL must be uploaded first'
456 codereview_url = changelist.GetCodereviewServer()
457 assert codereview_url, 'CL must be uploaded first'
458 patchset = patchset or changelist.GetMostRecentPatchset()
459 assert patchset, 'CL must be uploaded first'
460
461 codereview_host = urlparse.urlparse(codereview_url).hostname
462 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000463 http = authenticator.authorize(httplib2.Http())
464 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700465
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000466 buildbucket_put_url = (
467 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000468 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700469 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
470 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
471 hostname=codereview_host,
472 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000473 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700474
475 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
476 shared_parameters_properties['category'] = category
477 if options.clobber:
478 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700479 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700480 if extra_properties:
481 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000482
483 batch_req_body = {'builds': []}
484 print_text = []
485 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700486 for bucket, builders_and_tests in sorted(buckets.iteritems()):
487 print_text.append('Bucket: %s' % bucket)
488 master = None
489 if bucket.startswith(MASTER_PREFIX):
490 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000491 for builder, tests in sorted(builders_and_tests.iteritems()):
492 print_text.append(' %s: %s' % (builder, tests))
493 parameters = {
494 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000495 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100496 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000497 'revision': options.revision,
498 }],
tandrii8c5a3532016-11-04 07:52:02 -0700499 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000500 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000501 if 'presubmit' in builder.lower():
502 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000503 if tests:
504 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700505
506 tags = [
507 'builder:%s' % builder,
508 'buildset:%s' % buildset,
509 'user_agent:git_cl_try',
510 ]
511 if master:
512 parameters['properties']['master'] = master
513 tags.append('master:%s' % master)
514
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000515 batch_req_body['builds'].append(
516 {
517 'bucket': bucket,
518 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000519 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700520 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000521 }
522 )
523
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000524 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700525 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000526 http,
527 buildbucket_put_url,
528 'PUT',
529 body=json.dumps(batch_req_body),
530 headers={'Content-Type': 'application/json'}
531 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000532 print_text.append('To see results here, run: git cl try-results')
533 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700534 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000535
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000536
tandrii221ab252016-10-06 08:12:04 -0700537def fetch_try_jobs(auth_config, changelist, buildbucket_host,
538 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700539 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000540
qyearsley53f48a12016-09-01 10:45:13 -0700541 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000542 """
tandrii221ab252016-10-06 08:12:04 -0700543 assert buildbucket_host
544 assert changelist.GetIssue(), 'CL must be uploaded first'
545 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
546 patchset = patchset or changelist.GetMostRecentPatchset()
547 assert patchset, 'CL must be uploaded first'
548
549 codereview_url = changelist.GetCodereviewServer()
550 codereview_host = urlparse.urlparse(codereview_url).hostname
551 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 if authenticator.has_cached_credentials():
553 http = authenticator.authorize(httplib2.Http())
554 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700555 print('Warning: Some results might be missing because %s' %
556 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700557 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 http = httplib2.Http()
559
560 http.force_exception_to_status_code = True
561
tandrii221ab252016-10-06 08:12:04 -0700562 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
563 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
564 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700566 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567 params = {'tag': 'buildset:%s' % buildset}
568
569 builds = {}
570 while True:
571 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700572 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700574 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000575 for build in content.get('builds', []):
576 builds[build['id']] = build
577 if 'next_cursor' in content:
578 params['start_cursor'] = content['next_cursor']
579 else:
580 break
581 return builds
582
583
qyearsleyeab3c042016-08-24 09:18:28 -0700584def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 """Prints nicely result of fetch_try_jobs."""
586 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700587 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000588 return
589
590 # Make a copy, because we'll be modifying builds dictionary.
591 builds = builds.copy()
592 builder_names_cache = {}
593
594 def get_builder(b):
595 try:
596 return builder_names_cache[b['id']]
597 except KeyError:
598 try:
599 parameters = json.loads(b['parameters_json'])
600 name = parameters['builder_name']
601 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700602 print('WARNING: failed to get builder name for build %s: %s' % (
603 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000604 name = None
605 builder_names_cache[b['id']] = name
606 return name
607
608 def get_bucket(b):
609 bucket = b['bucket']
610 if bucket.startswith('master.'):
611 return bucket[len('master.'):]
612 return bucket
613
614 if options.print_master:
615 name_fmt = '%%-%ds %%-%ds' % (
616 max(len(str(get_bucket(b))) for b in builds.itervalues()),
617 max(len(str(get_builder(b))) for b in builds.itervalues()))
618 def get_name(b):
619 return name_fmt % (get_bucket(b), get_builder(b))
620 else:
621 name_fmt = '%%-%ds' % (
622 max(len(str(get_builder(b))) for b in builds.itervalues()))
623 def get_name(b):
624 return name_fmt % get_builder(b)
625
626 def sort_key(b):
627 return b['status'], b.get('result'), get_name(b), b.get('url')
628
629 def pop(title, f, color=None, **kwargs):
630 """Pop matching builds from `builds` dict and print them."""
631
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000632 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000633 colorize = str
634 else:
635 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
636
637 result = []
638 for b in builds.values():
639 if all(b.get(k) == v for k, v in kwargs.iteritems()):
640 builds.pop(b['id'])
641 result.append(b)
642 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700643 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000644 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700645 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000646
647 total = len(builds)
648 pop(status='COMPLETED', result='SUCCESS',
649 title='Successes:', color=Fore.GREEN,
650 f=lambda b: (get_name(b), b.get('url')))
651 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
652 title='Infra Failures:', color=Fore.MAGENTA,
653 f=lambda b: (get_name(b), b.get('url')))
654 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
655 title='Failures:', color=Fore.RED,
656 f=lambda b: (get_name(b), b.get('url')))
657 pop(status='COMPLETED', result='CANCELED',
658 title='Canceled:', color=Fore.MAGENTA,
659 f=lambda b: (get_name(b),))
660 pop(status='COMPLETED', result='FAILURE',
661 failure_reason='INVALID_BUILD_DEFINITION',
662 title='Wrong master/builder name:', color=Fore.MAGENTA,
663 f=lambda b: (get_name(b),))
664 pop(status='COMPLETED', result='FAILURE',
665 title='Other failures:',
666 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
667 pop(status='COMPLETED',
668 title='Other finished:',
669 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
670 pop(status='STARTED',
671 title='Started:', color=Fore.YELLOW,
672 f=lambda b: (get_name(b), b.get('url')))
673 pop(status='SCHEDULED',
674 title='Scheduled:',
675 f=lambda b: (get_name(b), 'id=%s' % b['id']))
676 # The last section is just in case buildbucket API changes OR there is a bug.
677 pop(title='Other:',
678 f=lambda b: (get_name(b), 'id=%s' % b['id']))
679 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700680 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000681
682
qyearsley53f48a12016-09-01 10:45:13 -0700683def write_try_results_json(output_file, builds):
684 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
685
686 The input |builds| dict is assumed to be generated by Buildbucket.
687 Buildbucket documentation: http://goo.gl/G0s101
688 """
689
690 def convert_build_dict(build):
691 return {
692 'buildbucket_id': build.get('id'),
693 'status': build.get('status'),
694 'result': build.get('result'),
695 'bucket': build.get('bucket'),
696 'builder_name': json.loads(
697 build.get('parameters_json', '{}')).get('builder_name'),
698 'failure_reason': build.get('failure_reason'),
699 'url': build.get('url'),
700 }
701
702 converted = []
703 for _, build in sorted(builds.items()):
704 converted.append(convert_build_dict(build))
705 write_json(output_file, converted)
706
707
iannucci@chromium.org79540052012-10-19 23:15:26 +0000708def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000709 """Prints statistics about the change to the user."""
710 # --no-ext-diff is broken in some versions of Git, so try to work around
711 # this by overriding the environment (but there is still a problem if the
712 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000713 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000714 if 'GIT_EXTERNAL_DIFF' in env:
715 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000716
717 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800718 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000719 else:
720 similarity_options = ['-M%s' % similarity]
721
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000722 try:
723 stdout = sys.stdout.fileno()
724 except AttributeError:
725 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000726 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000727 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000728 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000729 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000730
731
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000732class BuildbucketResponseException(Exception):
733 pass
734
735
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736class Settings(object):
737 def __init__(self):
738 self.default_server = None
739 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000740 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741 self.tree_status_url = None
742 self.viewvc_url = None
743 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000744 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000745 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000746 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000747 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000748 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000749 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750
751 def LazyUpdateIfNeeded(self):
752 """Updates the settings from a codereview.settings file, if available."""
753 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000754 # The only value that actually changes the behavior is
755 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000756 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000757 error_ok=True
758 ).strip().lower()
759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000761 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762 LoadCodereviewSettingsFromFile(cr_settings_file)
763 self.updated = True
764
765 def GetDefaultServerUrl(self, error_ok=False):
766 if not self.default_server:
767 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000768 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000769 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 if error_ok:
771 return self.default_server
772 if not self.default_server:
773 error_message = ('Could not find settings file. You must configure '
774 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000775 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000776 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777 return self.default_server
778
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000779 @staticmethod
780 def GetRelativeRoot():
781 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000782
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000784 if self.root is None:
785 self.root = os.path.abspath(self.GetRelativeRoot())
786 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000788 def GetGitMirror(self, remote='origin'):
789 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000790 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000791 if not os.path.isdir(local_url):
792 return None
793 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
794 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100795 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100796 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000797 if mirror.exists():
798 return mirror
799 return None
800
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 def GetTreeStatusUrl(self, error_ok=False):
802 if not self.tree_status_url:
803 error_message = ('You must configure your tree status URL by running '
804 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 self.tree_status_url = self._GetRietveldConfig(
806 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 return self.tree_status_url
808
809 def GetViewVCUrl(self):
810 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 return self.viewvc_url
813
rmistry@google.com90752582014-01-14 21:04:50 +0000814 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000815 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000816
rmistry@google.com78948ed2015-07-08 23:09:57 +0000817 def GetIsSkipDependencyUpload(self, branch_name):
818 """Returns true if specified branch should skip dep uploads."""
819 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
820 error_ok=True)
821
rmistry@google.com5626a922015-02-26 14:03:30 +0000822 def GetRunPostUploadHook(self):
823 run_post_upload_hook = self._GetRietveldConfig(
824 'run-post-upload-hook', error_ok=True)
825 return run_post_upload_hook == "True"
826
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000827 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000828 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000829
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000830 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000831 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000832
ukai@chromium.orge8077812012-02-03 03:41:46 +0000833 def GetIsGerrit(self):
834 """Return true if this repo is assosiated with gerrit code review system."""
835 if self.is_gerrit is None:
836 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
837 return self.is_gerrit
838
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000839 def GetSquashGerritUploads(self):
840 """Return true if uploads to Gerrit should be squashed by default."""
841 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700842 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
843 if self.squash_gerrit_uploads is None:
844 # Default is squash now (http://crbug.com/611892#c23).
845 self.squash_gerrit_uploads = not (
846 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
847 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000848 return self.squash_gerrit_uploads
849
tandriia60502f2016-06-20 02:01:53 -0700850 def GetSquashGerritUploadsOverride(self):
851 """Return True or False if codereview.settings should be overridden.
852
853 Returns None if no override has been defined.
854 """
855 # See also http://crbug.com/611892#c23
856 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
857 error_ok=True).strip()
858 if result == 'true':
859 return True
860 if result == 'false':
861 return False
862 return None
863
tandrii@chromium.org28253532016-04-14 13:46:56 +0000864 def GetGerritSkipEnsureAuthenticated(self):
865 """Return True if EnsureAuthenticated should not be done for Gerrit
866 uploads."""
867 if self.gerrit_skip_ensure_authenticated is None:
868 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000869 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000870 error_ok=True).strip() == 'true')
871 return self.gerrit_skip_ensure_authenticated
872
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000873 def GetGitEditor(self):
874 """Return the editor specified in the git config, or None if none is."""
875 if self.git_editor is None:
876 self.git_editor = self._GetConfig('core.editor', error_ok=True)
877 return self.git_editor or None
878
thestig@chromium.org44202a22014-03-11 19:22:18 +0000879 def GetLintRegex(self):
880 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
881 DEFAULT_LINT_REGEX)
882
883 def GetLintIgnoreRegex(self):
884 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
885 DEFAULT_LINT_IGNORE_REGEX)
886
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000887 def GetProject(self):
888 if not self.project:
889 self.project = self._GetRietveldConfig('project', error_ok=True)
890 return self.project
891
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000892 def _GetRietveldConfig(self, param, **kwargs):
893 return self._GetConfig('rietveld.' + param, **kwargs)
894
rmistry@google.com78948ed2015-07-08 23:09:57 +0000895 def _GetBranchConfig(self, branch_name, param, **kwargs):
896 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
897
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898 def _GetConfig(self, param, **kwargs):
899 self.LazyUpdateIfNeeded()
900 return RunGit(['config', param], **kwargs).strip()
901
902
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100903@contextlib.contextmanager
904def _get_gerrit_project_config_file(remote_url):
905 """Context manager to fetch and store Gerrit's project.config from
906 refs/meta/config branch and store it in temp file.
907
908 Provides a temporary filename or None if there was error.
909 """
910 error, _ = RunGitWithCode([
911 'fetch', remote_url,
912 '+refs/meta/config:refs/git_cl/meta/config'])
913 if error:
914 # Ref doesn't exist or isn't accessible to current user.
915 print('WARNING: failed to fetch project config for %s: %s' %
916 (remote_url, error))
917 yield None
918 return
919
920 error, project_config_data = RunGitWithCode(
921 ['show', 'refs/git_cl/meta/config:project.config'])
922 if error:
923 print('WARNING: project.config file not found')
924 yield None
925 return
926
927 with gclient_utils.temporary_directory() as tempdir:
928 project_config_file = os.path.join(tempdir, 'project.config')
929 gclient_utils.FileWrite(project_config_file, project_config_data)
930 yield project_config_file
931
932
933def _is_git_numberer_enabled(remote_url, remote_ref):
934 """Returns True if Git Numberer is enabled on this ref."""
935 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100936 KNOWN_PROJECTS_WHITELIST = [
937 'chromium/src',
938 'external/webrtc',
939 'v8/v8',
940 ]
941
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100942 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
943 url_parts = urlparse.urlparse(remote_url)
944 project_name = url_parts.path.lstrip('/').rstrip('git./')
945 for known in KNOWN_PROJECTS_WHITELIST:
946 if project_name.endswith(known):
947 break
948 else:
949 # Early exit to avoid extra fetches for repos that aren't using Git
950 # Numberer.
951 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100952
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100953 with _get_gerrit_project_config_file(remote_url) as project_config_file:
954 if project_config_file is None:
955 # Failed to fetch project.config, which shouldn't happen on open source
956 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100957 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100958 def get_opts(x):
959 code, out = RunGitWithCode(
960 ['config', '-f', project_config_file, '--get-all',
961 'plugin.git-numberer.validate-%s-refglob' % x])
962 if code == 0:
963 return out.strip().splitlines()
964 return []
965 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100966
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100967 logging.info('validator config enabled %s disabled %s refglobs for '
968 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000969
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100970 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100971 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100972 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100973 return True
974 return False
975
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100976 if match_refglobs(disabled):
977 return False
978 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100979
980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981def ShortBranchName(branch):
982 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000983 return branch.replace('refs/heads/', '', 1)
984
985
986def GetCurrentBranchRef():
987 """Returns branch ref (e.g., refs/heads/master) or None."""
988 return RunGit(['symbolic-ref', 'HEAD'],
989 stderr=subprocess2.VOID, error_ok=True).strip() or None
990
991
992def GetCurrentBranch():
993 """Returns current branch or None.
994
995 For refs/heads/* branches, returns just last part. For others, full ref.
996 """
997 branchref = GetCurrentBranchRef()
998 if branchref:
999 return ShortBranchName(branchref)
1000 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001
1002
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001003class _CQState(object):
1004 """Enum for states of CL with respect to Commit Queue."""
1005 NONE = 'none'
1006 DRY_RUN = 'dry_run'
1007 COMMIT = 'commit'
1008
1009 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1010
1011
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001012class _ParsedIssueNumberArgument(object):
1013 def __init__(self, issue=None, patchset=None, hostname=None):
1014 self.issue = issue
1015 self.patchset = patchset
1016 self.hostname = hostname
1017
1018 @property
1019 def valid(self):
1020 return self.issue is not None
1021
1022
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001023def ParseIssueNumberArgument(arg):
1024 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1025 fail_result = _ParsedIssueNumberArgument()
1026
1027 if arg.isdigit():
1028 return _ParsedIssueNumberArgument(issue=int(arg))
1029 if not arg.startswith('http'):
1030 return fail_result
1031 url = gclient_utils.UpgradeToHttps(arg)
1032 try:
1033 parsed_url = urlparse.urlparse(url)
1034 except ValueError:
1035 return fail_result
1036 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1037 tmp = cls.ParseIssueURL(parsed_url)
1038 if tmp is not None:
1039 return tmp
1040 return fail_result
1041
1042
Aaron Gablea45ee112016-11-22 15:14:38 -08001043class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001044 def __init__(self, issue, url):
1045 self.issue = issue
1046 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001047 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001048
1049 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001050 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001051 self.issue, self.url)
1052
1053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001055 """Changelist works with one changelist in local branch.
1056
1057 Supports two codereview backends: Rietveld or Gerrit, selected at object
1058 creation.
1059
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001060 Notes:
1061 * Not safe for concurrent multi-{thread,process} use.
1062 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001063 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001064 """
1065
1066 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1067 """Create a new ChangeList instance.
1068
1069 If issue is given, the codereview must be given too.
1070
1071 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1072 Otherwise, it's decided based on current configuration of the local branch,
1073 with default being 'rietveld' for backwards compatibility.
1074 See _load_codereview_impl for more details.
1075
1076 **kwargs will be passed directly to codereview implementation.
1077 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001079 global settings
1080 if not settings:
1081 # Happens when git_cl.py is used as a utility library.
1082 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001083
1084 if issue:
1085 assert codereview, 'codereview must be known, if issue is known'
1086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087 self.branchref = branchref
1088 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001089 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 self.branch = ShortBranchName(self.branchref)
1091 else:
1092 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001094 self.lookedup_issue = False
1095 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001096 self.has_description = False
1097 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001098 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001100 self.cc = None
1101 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001102 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001103
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001104 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001105 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001106 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001107 assert self._codereview_impl
1108 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001109
1110 def _load_codereview_impl(self, codereview=None, **kwargs):
1111 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001112 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1113 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1114 self._codereview = codereview
1115 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116 return
1117
1118 # Automatic selection based on issue number set for a current branch.
1119 # Rietveld takes precedence over Gerrit.
1120 assert not self.issue
1121 # Whether we find issue or not, we are doing the lookup.
1122 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001123 if self.GetBranch():
1124 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1125 issue = _git_get_branch_config_value(
1126 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1127 if issue:
1128 self._codereview = codereview
1129 self._codereview_impl = cls(self, **kwargs)
1130 self.issue = int(issue)
1131 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001132
1133 # No issue is set for this branch, so decide based on repo-wide settings.
1134 return self._load_codereview_impl(
1135 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1136 **kwargs)
1137
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001138 def IsGerrit(self):
1139 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001140
1141 def GetCCList(self):
1142 """Return the users cc'd on this CL.
1143
agable92bec4f2016-08-24 09:27:27 -07001144 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001145 """
1146 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001147 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001148 more_cc = ','.join(self.watchers)
1149 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1150 return self.cc
1151
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001152 def GetCCListWithoutDefault(self):
1153 """Return the users cc'd on this CL excluding default ones."""
1154 if self.cc is None:
1155 self.cc = ','.join(self.watchers)
1156 return self.cc
1157
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001158 def SetWatchers(self, watchers):
1159 """Set the list of email addresses that should be cc'd based on the changed
1160 files in this CL.
1161 """
1162 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163
1164 def GetBranch(self):
1165 """Returns the short branch name, e.g. 'master'."""
1166 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001167 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001168 if not branchref:
1169 return None
1170 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171 self.branch = ShortBranchName(self.branchref)
1172 return self.branch
1173
1174 def GetBranchRef(self):
1175 """Returns the full branch name, e.g. 'refs/heads/master'."""
1176 self.GetBranch() # Poke the lazy loader.
1177 return self.branchref
1178
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001179 def ClearBranch(self):
1180 """Clears cached branch data of this object."""
1181 self.branch = self.branchref = None
1182
tandrii5d48c322016-08-18 16:19:37 -07001183 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1184 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1185 kwargs['branch'] = self.GetBranch()
1186 return _git_get_branch_config_value(key, default, **kwargs)
1187
1188 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1189 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1190 assert self.GetBranch(), (
1191 'this CL must have an associated branch to %sset %s%s' %
1192 ('un' if value is None else '',
1193 key,
1194 '' if value is None else ' to %r' % value))
1195 kwargs['branch'] = self.GetBranch()
1196 return _git_set_branch_config_value(key, value, **kwargs)
1197
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001198 @staticmethod
1199 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001200 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 e.g. 'origin', 'refs/heads/master'
1202 """
1203 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001204 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1205
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001207 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001209 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1210 error_ok=True).strip()
1211 if upstream_branch:
1212 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001214 # Else, try to guess the origin remote.
1215 remote_branches = RunGit(['branch', '-r']).split()
1216 if 'origin/master' in remote_branches:
1217 # Fall back on origin/master if it exits.
1218 remote = 'origin'
1219 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001221 DieWithError(
1222 'Unable to determine default branch to diff against.\n'
1223 'Either pass complete "git diff"-style arguments, like\n'
1224 ' git cl upload origin/master\n'
1225 'or verify this branch is set up to track another \n'
1226 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227
1228 return remote, upstream_branch
1229
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001230 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001231 upstream_branch = self.GetUpstreamBranch()
1232 if not BranchExists(upstream_branch):
1233 DieWithError('The upstream for the current branch (%s) does not exist '
1234 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001235 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001236 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001237
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 def GetUpstreamBranch(self):
1239 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001240 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001242 upstream_branch = upstream_branch.replace('refs/heads/',
1243 'refs/remotes/%s/' % remote)
1244 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1245 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 self.upstream_branch = upstream_branch
1247 return self.upstream_branch
1248
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001249 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001250 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001251 remote, branch = None, self.GetBranch()
1252 seen_branches = set()
1253 while branch not in seen_branches:
1254 seen_branches.add(branch)
1255 remote, branch = self.FetchUpstreamTuple(branch)
1256 branch = ShortBranchName(branch)
1257 if remote != '.' or branch.startswith('refs/remotes'):
1258 break
1259 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001260 remotes = RunGit(['remote'], error_ok=True).split()
1261 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001262 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001263 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001264 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001265 logging.warn('Could not determine which remote this change is '
1266 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001267 else:
1268 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001269 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 branch = 'HEAD'
1271 if branch.startswith('refs/remotes'):
1272 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001273 elif branch.startswith('refs/branch-heads/'):
1274 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001275 else:
1276 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001277 return self._remote
1278
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001279 def GitSanityChecks(self, upstream_git_obj):
1280 """Checks git repo status and ensures diff is from local commits."""
1281
sbc@chromium.org79706062015-01-14 21:18:12 +00001282 if upstream_git_obj is None:
1283 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001284 print('ERROR: unable to determine current branch (detached HEAD?)',
1285 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001286 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001287 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001288 return False
1289
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 # Verify the commit we're diffing against is in our current branch.
1291 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1292 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1293 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001294 print('ERROR: %s is not in the current branch. You may need to rebase '
1295 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001296 return False
1297
1298 # List the commits inside the diff, and verify they are all local.
1299 commits_in_diff = RunGit(
1300 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1301 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1302 remote_branch = remote_branch.strip()
1303 if code != 0:
1304 _, remote_branch = self.GetRemoteBranch()
1305
1306 commits_in_remote = RunGit(
1307 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1308
1309 common_commits = set(commits_in_diff) & set(commits_in_remote)
1310 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001311 print('ERROR: Your diff contains %d commits already in %s.\n'
1312 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1313 'the diff. If you are using a custom git flow, you can override'
1314 ' the reference used for this check with "git config '
1315 'gitcl.remotebranch <git-ref>".' % (
1316 len(common_commits), remote_branch, upstream_git_obj),
1317 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001318 return False
1319 return True
1320
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001321 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001322 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001323
1324 Returns None if it is not set.
1325 """
tandrii5d48c322016-08-18 16:19:37 -07001326 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001327
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 def GetRemoteUrl(self):
1329 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1330
1331 Returns None if there is no remote.
1332 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001333 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001334 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1335
1336 # If URL is pointing to a local directory, it is probably a git cache.
1337 if os.path.isdir(url):
1338 url = RunGit(['config', 'remote.%s.url' % remote],
1339 error_ok=True,
1340 cwd=url).strip()
1341 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001343 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001344 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001345 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001346 self.issue = self._GitGetBranchConfigValue(
1347 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001348 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349 return self.issue
1350
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351 def GetIssueURL(self):
1352 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001353 issue = self.GetIssue()
1354 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001355 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001356 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001358 def GetDescription(self, pretty=False, force=False):
1359 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001361 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 self.has_description = True
1363 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001364 # Set width to 72 columns + 2 space indent.
1365 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001367 lines = self.description.splitlines()
1368 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 return self.description
1370
1371 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001372 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001373 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001374 self.patchset = self._GitGetBranchConfigValue(
1375 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001376 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 return self.patchset
1378
1379 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001380 """Set this branch's patchset. If patchset=0, clears the patchset."""
1381 assert self.GetBranch()
1382 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001383 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001384 else:
1385 self.patchset = int(patchset)
1386 self._GitSetBranchConfigValue(
1387 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001389 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001390 """Set this branch's issue. If issue isn't given, clears the issue."""
1391 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001393 issue = int(issue)
1394 self._GitSetBranchConfigValue(
1395 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001396 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001397 codereview_server = self._codereview_impl.GetCodereviewServer()
1398 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001399 self._GitSetBranchConfigValue(
1400 self._codereview_impl.CodereviewServerConfigKey(),
1401 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 else:
tandrii5d48c322016-08-18 16:19:37 -07001403 # Reset all of these just to be clean.
1404 reset_suffixes = [
1405 'last-upload-hash',
1406 self._codereview_impl.IssueConfigKey(),
1407 self._codereview_impl.PatchsetConfigKey(),
1408 self._codereview_impl.CodereviewServerConfigKey(),
1409 ] + self._PostUnsetIssueProperties()
1410 for prop in reset_suffixes:
1411 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001412 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001413 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414
dnjba1b0f32016-09-02 12:37:42 -07001415 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001416 if not self.GitSanityChecks(upstream_branch):
1417 DieWithError('\nGit sanity check failure')
1418
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001419 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001420 if not root:
1421 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001422 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001423
1424 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001425 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001426 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001427 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001428 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001429 except subprocess2.CalledProcessError:
1430 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001431 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001432 'This branch probably doesn\'t exist anymore. To reset the\n'
1433 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001434 ' git branch --set-upstream-to origin/master %s\n'
1435 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001436 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001437
maruel@chromium.org52424302012-08-29 15:14:30 +00001438 issue = self.GetIssue()
1439 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001440 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001441 description = self.GetDescription()
1442 else:
1443 # If the change was never uploaded, use the log messages of all commits
1444 # up to the branch point, as git cl upload will prefill the description
1445 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001446 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1447 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001448
1449 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001450 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001451 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001452 name,
1453 description,
1454 absroot,
1455 files,
1456 issue,
1457 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001458 author,
1459 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001460
dsansomee2d6fd92016-09-08 00:10:47 -07001461 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001462 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001463 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001464 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001465
1466 def RunHook(self, committing, may_prompt, verbose, change):
1467 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1468 try:
1469 return presubmit_support.DoPresubmitChecks(change, committing,
1470 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1471 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001472 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1473 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001474 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001475 DieWithError(
1476 ('%s\nMaybe your depot_tools is out of date?\n'
1477 'If all fails, contact maruel@') % e)
1478
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001479 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1480 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001481 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1482 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001483 else:
1484 # Assume url.
1485 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1486 urlparse.urlparse(issue_arg))
1487 if not parsed_issue_arg or not parsed_issue_arg.valid:
1488 DieWithError('Failed to parse issue argument "%s". '
1489 'Must be an issue number or a valid URL.' % issue_arg)
1490 return self._codereview_impl.CMDPatchWithParsedIssue(
1491 parsed_issue_arg, reject, nocommit, directory)
1492
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001493 def CMDUpload(self, options, git_diff_args, orig_args):
1494 """Uploads a change to codereview."""
1495 if git_diff_args:
1496 # TODO(ukai): is it ok for gerrit case?
1497 base_branch = git_diff_args[0]
1498 else:
1499 if self.GetBranch() is None:
1500 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1501
1502 # Default to diffing against common ancestor of upstream branch
1503 base_branch = self.GetCommonAncestorWithUpstream()
1504 git_diff_args = [base_branch, 'HEAD']
1505
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001506 # Fast best-effort checks to abort before running potentially
1507 # expensive hooks if uploading is likely to fail anyway. Passing these
1508 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001509 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001510 self._codereview_impl.EnsureCanUploadPatchset()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001511
1512 # Apply watchlists on upload.
1513 change = self.GetChange(base_branch, None)
1514 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1515 files = [f.LocalPath() for f in change.AffectedFiles()]
1516 if not options.bypass_watchlists:
1517 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1518
1519 if not options.bypass_hooks:
1520 if options.reviewers or options.tbr_owners:
1521 # Set the reviewer list now so that presubmit checks can access it.
1522 change_description = ChangeDescription(change.FullDescriptionText())
1523 change_description.update_reviewers(options.reviewers,
1524 options.tbr_owners,
1525 change)
1526 change.SetDescriptionText(change_description.description)
1527 hook_results = self.RunHook(committing=False,
1528 may_prompt=not options.force,
1529 verbose=options.verbose,
1530 change=change)
1531 if not hook_results.should_continue():
1532 return 1
1533 if not options.reviewers and hook_results.reviewers:
1534 options.reviewers = hook_results.reviewers.split(',')
1535
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001536 # TODO(tandrii): Checking local patchset against remote patchset is only
1537 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1538 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001539 latest_patchset = self.GetMostRecentPatchset()
1540 local_patchset = self.GetPatchset()
1541 if (latest_patchset and local_patchset and
1542 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001543 print('The last upload made from this repository was patchset #%d but '
1544 'the most recent patchset on the server is #%d.'
1545 % (local_patchset, latest_patchset))
1546 print('Uploading will still work, but if you\'ve uploaded to this '
1547 'issue from another machine or branch the patch you\'re '
1548 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001549 ask_for_data('About to upload; enter to confirm.')
1550
1551 print_stats(options.similarity, options.find_copies, git_diff_args)
1552 ret = self.CMDUploadChange(options, git_diff_args, change)
1553 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001554 if options.use_commit_queue:
1555 self.SetCQState(_CQState.COMMIT)
1556 elif options.cq_dry_run:
1557 self.SetCQState(_CQState.DRY_RUN)
1558
tandrii5d48c322016-08-18 16:19:37 -07001559 _git_set_branch_config_value('last-upload-hash',
1560 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001561 # Run post upload hooks, if specified.
1562 if settings.GetRunPostUploadHook():
1563 presubmit_support.DoPostUploadExecuter(
1564 change,
1565 self,
1566 settings.GetRoot(),
1567 options.verbose,
1568 sys.stdout)
1569
1570 # Upload all dependencies if specified.
1571 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001572 print()
1573 print('--dependencies has been specified.')
1574 print('All dependent local branches will be re-uploaded.')
1575 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001576 # Remove the dependencies flag from args so that we do not end up in a
1577 # loop.
1578 orig_args.remove('--dependencies')
1579 ret = upload_branch_deps(self, orig_args)
1580 return ret
1581
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001582 def SetCQState(self, new_state):
1583 """Update the CQ state for latest patchset.
1584
1585 Issue must have been already uploaded and known.
1586 """
1587 assert new_state in _CQState.ALL_STATES
1588 assert self.GetIssue()
1589 return self._codereview_impl.SetCQState(new_state)
1590
qyearsley1fdfcb62016-10-24 13:22:03 -07001591 def TriggerDryRun(self):
1592 """Triggers a dry run and prints a warning on failure."""
1593 # TODO(qyearsley): Either re-use this method in CMDset_commit
1594 # and CMDupload, or change CMDtry to trigger dry runs with
1595 # just SetCQState, and catch keyboard interrupt and other
1596 # errors in that method.
1597 try:
1598 self.SetCQState(_CQState.DRY_RUN)
1599 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1600 return 0
1601 except KeyboardInterrupt:
1602 raise
1603 except:
1604 print('WARNING: failed to trigger CQ Dry Run.\n'
1605 'Either:\n'
1606 ' * your project has no CQ\n'
1607 ' * you don\'t have permission to trigger Dry Run\n'
1608 ' * bug in this code (see stack trace below).\n'
1609 'Consider specifying which bots to trigger manually '
1610 'or asking your project owners for permissions '
1611 'or contacting Chrome Infrastructure team at '
1612 'https://www.chromium.org/infra\n\n')
1613 # Still raise exception so that stack trace is printed.
1614 raise
1615
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001616 # Forward methods to codereview specific implementation.
1617
1618 def CloseIssue(self):
1619 return self._codereview_impl.CloseIssue()
1620
1621 def GetStatus(self):
1622 return self._codereview_impl.GetStatus()
1623
1624 def GetCodereviewServer(self):
1625 return self._codereview_impl.GetCodereviewServer()
1626
tandriide281ae2016-10-12 06:02:30 -07001627 def GetIssueOwner(self):
1628 """Get owner from codereview, which may differ from this checkout."""
1629 return self._codereview_impl.GetIssueOwner()
1630
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001631 def GetApprovingReviewers(self):
1632 return self._codereview_impl.GetApprovingReviewers()
1633
1634 def GetMostRecentPatchset(self):
1635 return self._codereview_impl.GetMostRecentPatchset()
1636
tandriide281ae2016-10-12 06:02:30 -07001637 def CannotTriggerTryJobReason(self):
1638 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1639 return self._codereview_impl.CannotTriggerTryJobReason()
1640
tandrii8c5a3532016-11-04 07:52:02 -07001641 def GetTryjobProperties(self, patchset=None):
1642 """Returns dictionary of properties to launch tryjob."""
1643 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1644
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001645 def __getattr__(self, attr):
1646 # This is because lots of untested code accesses Rietveld-specific stuff
1647 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001648 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001649 # Note that child method defines __getattr__ as well, and forwards it here,
1650 # because _RietveldChangelistImpl is not cleaned up yet, and given
1651 # deprecation of Rietveld, it should probably be just removed.
1652 # Until that time, avoid infinite recursion by bypassing __getattr__
1653 # of implementation class.
1654 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001655
1656
1657class _ChangelistCodereviewBase(object):
1658 """Abstract base class encapsulating codereview specifics of a changelist."""
1659 def __init__(self, changelist):
1660 self._changelist = changelist # instance of Changelist
1661
1662 def __getattr__(self, attr):
1663 # Forward methods to changelist.
1664 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1665 # _RietveldChangelistImpl to avoid this hack?
1666 return getattr(self._changelist, attr)
1667
1668 def GetStatus(self):
1669 """Apply a rough heuristic to give a simple summary of an issue's review
1670 or CQ status, assuming adherence to a common workflow.
1671
1672 Returns None if no issue for this branch, or specific string keywords.
1673 """
1674 raise NotImplementedError()
1675
1676 def GetCodereviewServer(self):
1677 """Returns server URL without end slash, like "https://codereview.com"."""
1678 raise NotImplementedError()
1679
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001680 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001681 """Fetches and returns description from the codereview server."""
1682 raise NotImplementedError()
1683
tandrii5d48c322016-08-18 16:19:37 -07001684 @classmethod
1685 def IssueConfigKey(cls):
1686 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001687 raise NotImplementedError()
1688
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001689 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001690 def PatchsetConfigKey(cls):
1691 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001692 raise NotImplementedError()
1693
tandrii5d48c322016-08-18 16:19:37 -07001694 @classmethod
1695 def CodereviewServerConfigKey(cls):
1696 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001697 raise NotImplementedError()
1698
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001699 def _PostUnsetIssueProperties(self):
1700 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001701 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001702
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001703 def GetRieveldObjForPresubmit(self):
1704 # This is an unfortunate Rietveld-embeddedness in presubmit.
1705 # For non-Rietveld codereviews, this probably should return a dummy object.
1706 raise NotImplementedError()
1707
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001708 def GetGerritObjForPresubmit(self):
1709 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1710 return None
1711
dsansomee2d6fd92016-09-08 00:10:47 -07001712 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001713 """Update the description on codereview site."""
1714 raise NotImplementedError()
1715
1716 def CloseIssue(self):
1717 """Closes the issue."""
1718 raise NotImplementedError()
1719
1720 def GetApprovingReviewers(self):
1721 """Returns a list of reviewers approving the change.
1722
1723 Note: not necessarily committers.
1724 """
1725 raise NotImplementedError()
1726
1727 def GetMostRecentPatchset(self):
1728 """Returns the most recent patchset number from the codereview site."""
1729 raise NotImplementedError()
1730
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001731 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1732 directory):
1733 """Fetches and applies the issue.
1734
1735 Arguments:
1736 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1737 reject: if True, reject the failed patch instead of switching to 3-way
1738 merge. Rietveld only.
1739 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1740 only.
1741 directory: switch to directory before applying the patch. Rietveld only.
1742 """
1743 raise NotImplementedError()
1744
1745 @staticmethod
1746 def ParseIssueURL(parsed_url):
1747 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1748 failed."""
1749 raise NotImplementedError()
1750
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001751 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001752 """Best effort check that user is authenticated with codereview server.
1753
1754 Arguments:
1755 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001756 refresh: whether to attempt to refresh credentials. Ignored if not
1757 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001758 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001759 raise NotImplementedError()
1760
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001761 def EnsureCanUploadPatchset(self):
1762 """Best effort check that uploading isn't supposed to fail for predictable
1763 reasons.
1764
1765 This method should raise informative exception if uploading shouldn't
1766 proceed.
1767 """
1768 pass
1769
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001770 def CMDUploadChange(self, options, args, change):
1771 """Uploads a change to codereview."""
1772 raise NotImplementedError()
1773
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001774 def SetCQState(self, new_state):
1775 """Update the CQ state for latest patchset.
1776
1777 Issue must have been already uploaded and known.
1778 """
1779 raise NotImplementedError()
1780
tandriie113dfd2016-10-11 10:20:12 -07001781 def CannotTriggerTryJobReason(self):
1782 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1783 raise NotImplementedError()
1784
tandriide281ae2016-10-12 06:02:30 -07001785 def GetIssueOwner(self):
1786 raise NotImplementedError()
1787
tandrii8c5a3532016-11-04 07:52:02 -07001788 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001789 raise NotImplementedError()
1790
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001791
1792class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001793 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001794 super(_RietveldChangelistImpl, self).__init__(changelist)
1795 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001796 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001797 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001798
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001799 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001800 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001801 self._props = None
1802 self._rpc_server = None
1803
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001804 def GetCodereviewServer(self):
1805 if not self._rietveld_server:
1806 # If we're on a branch then get the server potentially associated
1807 # with that branch.
1808 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001809 self._rietveld_server = gclient_utils.UpgradeToHttps(
1810 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001811 if not self._rietveld_server:
1812 self._rietveld_server = settings.GetDefaultServerUrl()
1813 return self._rietveld_server
1814
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001815 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001816 """Best effort check that user is authenticated with Rietveld server."""
1817 if self._auth_config.use_oauth2:
1818 authenticator = auth.get_authenticator_for_host(
1819 self.GetCodereviewServer(), self._auth_config)
1820 if not authenticator.has_cached_credentials():
1821 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001822 if refresh:
1823 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001824
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001825 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001826 issue = self.GetIssue()
1827 assert issue
1828 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001829 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001830 except urllib2.HTTPError as e:
1831 if e.code == 404:
1832 DieWithError(
1833 ('\nWhile fetching the description for issue %d, received a '
1834 '404 (not found)\n'
1835 'error. It is likely that you deleted this '
1836 'issue on the server. If this is the\n'
1837 'case, please run\n\n'
1838 ' git cl issue 0\n\n'
1839 'to clear the association with the deleted issue. Then run '
1840 'this command again.') % issue)
1841 else:
1842 DieWithError(
1843 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1844 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001845 print('Warning: Failed to retrieve CL description due to network '
1846 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001847 return ''
1848
1849 def GetMostRecentPatchset(self):
1850 return self.GetIssueProperties()['patchsets'][-1]
1851
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001852 def GetIssueProperties(self):
1853 if self._props is None:
1854 issue = self.GetIssue()
1855 if not issue:
1856 self._props = {}
1857 else:
1858 self._props = self.RpcServer().get_issue_properties(issue, True)
1859 return self._props
1860
tandriie113dfd2016-10-11 10:20:12 -07001861 def CannotTriggerTryJobReason(self):
1862 props = self.GetIssueProperties()
1863 if not props:
1864 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1865 if props.get('closed'):
1866 return 'CL %s is closed' % self.GetIssue()
1867 if props.get('private'):
1868 return 'CL %s is private' % self.GetIssue()
1869 return None
1870
tandrii8c5a3532016-11-04 07:52:02 -07001871 def GetTryjobProperties(self, patchset=None):
1872 """Returns dictionary of properties to launch tryjob."""
1873 project = (self.GetIssueProperties() or {}).get('project')
1874 return {
1875 'issue': self.GetIssue(),
1876 'patch_project': project,
1877 'patch_storage': 'rietveld',
1878 'patchset': patchset or self.GetPatchset(),
1879 'rietveld': self.GetCodereviewServer(),
1880 }
1881
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001882 def GetApprovingReviewers(self):
1883 return get_approving_reviewers(self.GetIssueProperties())
1884
tandriide281ae2016-10-12 06:02:30 -07001885 def GetIssueOwner(self):
1886 return (self.GetIssueProperties() or {}).get('owner_email')
1887
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001888 def AddComment(self, message):
1889 return self.RpcServer().add_comment(self.GetIssue(), message)
1890
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001891 def GetStatus(self):
1892 """Apply a rough heuristic to give a simple summary of an issue's review
1893 or CQ status, assuming adherence to a common workflow.
1894
1895 Returns None if no issue for this branch, or one of the following keywords:
1896 * 'error' - error from review tool (including deleted issues)
1897 * 'unsent' - not sent for review
1898 * 'waiting' - waiting for review
1899 * 'reply' - waiting for owner to reply to review
1900 * 'lgtm' - LGTM from at least one approved reviewer
1901 * 'commit' - in the commit queue
1902 * 'closed' - closed
1903 """
1904 if not self.GetIssue():
1905 return None
1906
1907 try:
1908 props = self.GetIssueProperties()
1909 except urllib2.HTTPError:
1910 return 'error'
1911
1912 if props.get('closed'):
1913 # Issue is closed.
1914 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001915 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001916 # Issue is in the commit queue.
1917 return 'commit'
1918
1919 try:
1920 reviewers = self.GetApprovingReviewers()
1921 except urllib2.HTTPError:
1922 return 'error'
1923
1924 if reviewers:
1925 # Was LGTM'ed.
1926 return 'lgtm'
1927
1928 messages = props.get('messages') or []
1929
tandrii9d2c7a32016-06-22 03:42:45 -07001930 # Skip CQ messages that don't require owner's action.
1931 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1932 if 'Dry run:' in messages[-1]['text']:
1933 messages.pop()
1934 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1935 # This message always follows prior messages from CQ,
1936 # so skip this too.
1937 messages.pop()
1938 else:
1939 # This is probably a CQ messages warranting user attention.
1940 break
1941
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001942 if not messages:
1943 # No message was sent.
1944 return 'unsent'
1945 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001946 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001947 return 'reply'
1948 return 'waiting'
1949
dsansomee2d6fd92016-09-08 00:10:47 -07001950 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001951 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001952
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001953 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001954 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001955
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001956 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001957 return self.SetFlags({flag: value})
1958
1959 def SetFlags(self, flags):
1960 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001961 """
phajdan.jr68598232016-08-10 03:28:28 -07001962 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001963 try:
tandrii4b233bd2016-07-06 03:50:29 -07001964 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001965 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001966 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001967 if e.code == 404:
1968 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1969 if e.code == 403:
1970 DieWithError(
1971 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001972 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001973 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001975 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001976 """Returns an upload.RpcServer() to access this review's rietveld instance.
1977 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001978 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001979 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001980 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001981 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001982 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001983
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001984 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001985 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001986 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987
tandrii5d48c322016-08-18 16:19:37 -07001988 @classmethod
1989 def PatchsetConfigKey(cls):
1990 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001991
tandrii5d48c322016-08-18 16:19:37 -07001992 @classmethod
1993 def CodereviewServerConfigKey(cls):
1994 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001995
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001996 def GetRieveldObjForPresubmit(self):
1997 return self.RpcServer()
1998
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001999 def SetCQState(self, new_state):
2000 props = self.GetIssueProperties()
2001 if props.get('private'):
2002 DieWithError('Cannot set-commit on private issue')
2003
2004 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002005 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002006 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002007 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002008 else:
tandrii4b233bd2016-07-06 03:50:29 -07002009 assert new_state == _CQState.DRY_RUN
2010 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002011
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002012 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2013 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002014 # PatchIssue should never be called with a dirty tree. It is up to the
2015 # caller to check this, but just in case we assert here since the
2016 # consequences of the caller not checking this could be dire.
2017 assert(not git_common.is_dirty_git_tree('apply'))
2018 assert(parsed_issue_arg.valid)
2019 self._changelist.issue = parsed_issue_arg.issue
2020 if parsed_issue_arg.hostname:
2021 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2022
skobes6468b902016-10-24 08:45:10 -07002023 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2024 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2025 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002026 try:
skobes6468b902016-10-24 08:45:10 -07002027 scm_obj.apply_patch(patchset_object)
2028 except Exception as e:
2029 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002030 return 1
2031
2032 # If we had an issue, commit the current state and register the issue.
2033 if not nocommit:
2034 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2035 'patch from issue %(i)s at patchset '
2036 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2037 % {'i': self.GetIssue(), 'p': patchset})])
2038 self.SetIssue(self.GetIssue())
2039 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002040 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002041 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002042 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002043 return 0
2044
2045 @staticmethod
2046 def ParseIssueURL(parsed_url):
2047 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2048 return None
wychen3c1c1722016-08-04 11:46:36 -07002049 # Rietveld patch: https://domain/<number>/#ps<patchset>
2050 match = re.match(r'/(\d+)/$', parsed_url.path)
2051 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2052 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002053 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002054 issue=int(match.group(1)),
2055 patchset=int(match2.group(1)),
2056 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002057 # Typical url: https://domain/<issue_number>[/[other]]
2058 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2059 if match:
skobes6468b902016-10-24 08:45:10 -07002060 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002061 issue=int(match.group(1)),
2062 hostname=parsed_url.netloc)
2063 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2064 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2065 if match:
skobes6468b902016-10-24 08:45:10 -07002066 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002067 issue=int(match.group(1)),
2068 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002069 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002070 return None
2071
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002072 def CMDUploadChange(self, options, args, change):
2073 """Upload the patch to Rietveld."""
2074 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2075 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002076 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2077 if options.emulate_svn_auto_props:
2078 upload_args.append('--emulate_svn_auto_props')
2079
2080 change_desc = None
2081
2082 if options.email is not None:
2083 upload_args.extend(['--email', options.email])
2084
2085 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002086 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002087 upload_args.extend(['--title', options.title])
2088 if options.message:
2089 upload_args.extend(['--message', options.message])
2090 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002091 print('This branch is associated with issue %s. '
2092 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002093 else:
nodirca166002016-06-27 10:59:51 -07002094 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002095 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002096 if options.message:
2097 message = options.message
2098 else:
2099 message = CreateDescriptionFromLog(args)
2100 if options.title:
2101 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002102 change_desc = ChangeDescription(message)
2103 if options.reviewers or options.tbr_owners:
2104 change_desc.update_reviewers(options.reviewers,
2105 options.tbr_owners,
2106 change)
2107 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002108 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002109
2110 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002111 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002112 return 1
2113
2114 upload_args.extend(['--message', change_desc.description])
2115 if change_desc.get_reviewers():
2116 upload_args.append('--reviewers=%s' % ','.join(
2117 change_desc.get_reviewers()))
2118 if options.send_mail:
2119 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002120 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002121 upload_args.append('--send_mail')
2122
2123 # We check this before applying rietveld.private assuming that in
2124 # rietveld.cc only addresses which we can send private CLs to are listed
2125 # if rietveld.private is set, and so we should ignore rietveld.cc only
2126 # when --private is specified explicitly on the command line.
2127 if options.private:
2128 logging.warn('rietveld.cc is ignored since private flag is specified. '
2129 'You need to review and add them manually if necessary.')
2130 cc = self.GetCCListWithoutDefault()
2131 else:
2132 cc = self.GetCCList()
2133 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002134 if change_desc.get_cced():
2135 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002136 if cc:
2137 upload_args.extend(['--cc', cc])
2138
2139 if options.private or settings.GetDefaultPrivateFlag() == "True":
2140 upload_args.append('--private')
2141
2142 upload_args.extend(['--git_similarity', str(options.similarity)])
2143 if not options.find_copies:
2144 upload_args.extend(['--git_no_find_copies'])
2145
2146 # Include the upstream repo's URL in the change -- this is useful for
2147 # projects that have their source spread across multiple repos.
2148 remote_url = self.GetGitBaseUrlFromConfig()
2149 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002150 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2151 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2152 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002153 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002154 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002155 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002156 if target_ref:
2157 upload_args.extend(['--target_ref', target_ref])
2158
2159 # Look for dependent patchsets. See crbug.com/480453 for more details.
2160 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2161 upstream_branch = ShortBranchName(upstream_branch)
2162 if remote is '.':
2163 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002164 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002165 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002166 print()
2167 print('Skipping dependency patchset upload because git config '
2168 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2169 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002170 else:
2171 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002172 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002173 auth_config=auth_config)
2174 branch_cl_issue_url = branch_cl.GetIssueURL()
2175 branch_cl_issue = branch_cl.GetIssue()
2176 branch_cl_patchset = branch_cl.GetPatchset()
2177 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2178 upload_args.extend(
2179 ['--depends_on_patchset', '%s:%s' % (
2180 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002181 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002182 '\n'
2183 'The current branch (%s) is tracking a local branch (%s) with '
2184 'an associated CL.\n'
2185 'Adding %s/#ps%s as a dependency patchset.\n'
2186 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2187 branch_cl_patchset))
2188
2189 project = settings.GetProject()
2190 if project:
2191 upload_args.extend(['--project', project])
2192
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002193 try:
2194 upload_args = ['upload'] + upload_args + args
2195 logging.info('upload.RealMain(%s)', upload_args)
2196 issue, patchset = upload.RealMain(upload_args)
2197 issue = int(issue)
2198 patchset = int(patchset)
2199 except KeyboardInterrupt:
2200 sys.exit(1)
2201 except:
2202 # If we got an exception after the user typed a description for their
2203 # change, back up the description before re-raising.
2204 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002205 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002206 raise
2207
2208 if not self.GetIssue():
2209 self.SetIssue(issue)
2210 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211 return 0
2212
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002213
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002214class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002215 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002216 # auth_config is Rietveld thing, kept here to preserve interface only.
2217 super(_GerritChangelistImpl, self).__init__(changelist)
2218 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002219 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002220 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002221 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002222 # Map from change number (issue) to its detail cache.
2223 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002224
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002225 if codereview_host is not None:
2226 assert not codereview_host.startswith('https://'), codereview_host
2227 self._gerrit_host = codereview_host
2228 self._gerrit_server = 'https://%s' % codereview_host
2229
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002230 def _GetGerritHost(self):
2231 # Lazy load of configs.
2232 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002233 if self._gerrit_host and '.' not in self._gerrit_host:
2234 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2235 # This happens for internal stuff http://crbug.com/614312.
2236 parsed = urlparse.urlparse(self.GetRemoteUrl())
2237 if parsed.scheme == 'sso':
2238 print('WARNING: using non https URLs for remote is likely broken\n'
2239 ' Your current remote is: %s' % self.GetRemoteUrl())
2240 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2241 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002242 return self._gerrit_host
2243
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002244 def _GetGitHost(self):
2245 """Returns git host to be used when uploading change to Gerrit."""
2246 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2247
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002248 def GetCodereviewServer(self):
2249 if not self._gerrit_server:
2250 # If we're on a branch then get the server potentially associated
2251 # with that branch.
2252 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002253 self._gerrit_server = self._GitGetBranchConfigValue(
2254 self.CodereviewServerConfigKey())
2255 if self._gerrit_server:
2256 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002257 if not self._gerrit_server:
2258 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2259 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002260 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002261 parts[0] = parts[0] + '-review'
2262 self._gerrit_host = '.'.join(parts)
2263 self._gerrit_server = 'https://%s' % self._gerrit_host
2264 return self._gerrit_server
2265
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002266 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002267 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002268 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002269
tandrii5d48c322016-08-18 16:19:37 -07002270 @classmethod
2271 def PatchsetConfigKey(cls):
2272 return 'gerritpatchset'
2273
2274 @classmethod
2275 def CodereviewServerConfigKey(cls):
2276 return 'gerritserver'
2277
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002278 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002279 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002280 if settings.GetGerritSkipEnsureAuthenticated():
2281 # For projects with unusual authentication schemes.
2282 # See http://crbug.com/603378.
2283 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002284 # Lazy-loader to identify Gerrit and Git hosts.
2285 if gerrit_util.GceAuthenticator.is_gce():
2286 return
2287 self.GetCodereviewServer()
2288 git_host = self._GetGitHost()
2289 assert self._gerrit_server and self._gerrit_host
2290 cookie_auth = gerrit_util.CookiesAuthenticator()
2291
2292 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2293 git_auth = cookie_auth.get_auth_header(git_host)
2294 if gerrit_auth and git_auth:
2295 if gerrit_auth == git_auth:
2296 return
2297 print((
2298 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2299 ' Check your %s or %s file for credentials of hosts:\n'
2300 ' %s\n'
2301 ' %s\n'
2302 ' %s') %
2303 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2304 git_host, self._gerrit_host,
2305 cookie_auth.get_new_password_message(git_host)))
2306 if not force:
2307 ask_for_data('If you know what you are doing, press Enter to continue, '
2308 'Ctrl+C to abort.')
2309 return
2310 else:
2311 missing = (
2312 [] if gerrit_auth else [self._gerrit_host] +
2313 [] if git_auth else [git_host])
2314 DieWithError('Credentials for the following hosts are required:\n'
2315 ' %s\n'
2316 'These are read from %s (or legacy %s)\n'
2317 '%s' % (
2318 '\n '.join(missing),
2319 cookie_auth.get_gitcookies_path(),
2320 cookie_auth.get_netrc_path(),
2321 cookie_auth.get_new_password_message(git_host)))
2322
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002323 def EnsureCanUploadPatchset(self):
2324 """Best effort check that uploading isn't supposed to fail for predictable
2325 reasons.
2326
2327 This method should raise informative exception if uploading shouldn't
2328 proceed.
2329 """
2330 if not self.GetIssue():
2331 return
2332
2333 # Warm change details cache now to avoid RPCs later, reducing latency for
2334 # developers.
2335 self.FetchDescription()
2336
2337 status = self._GetChangeDetail()['status']
2338 if status in ('MERGED', 'ABANDONED'):
2339 DieWithError('Change %s has been %s, new uploads are not allowed' %
2340 (self.GetIssueURL(),
2341 'submitted' if status == 'MERGED' else 'abandoned'))
2342
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002343 def _PostUnsetIssueProperties(self):
2344 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002345 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002346
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002347 def GetRieveldObjForPresubmit(self):
2348 class ThisIsNotRietveldIssue(object):
2349 def __nonzero__(self):
2350 # This is a hack to make presubmit_support think that rietveld is not
2351 # defined, yet still ensure that calls directly result in a decent
2352 # exception message below.
2353 return False
2354
2355 def __getattr__(self, attr):
2356 print(
2357 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2358 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2359 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2360 'or use Rietveld for codereview.\n'
2361 'See also http://crbug.com/579160.' % attr)
2362 raise NotImplementedError()
2363 return ThisIsNotRietveldIssue()
2364
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002365 def GetGerritObjForPresubmit(self):
2366 return presubmit_support.GerritAccessor(self._GetGerritHost())
2367
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002368 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002369 """Apply a rough heuristic to give a simple summary of an issue's review
2370 or CQ status, assuming adherence to a common workflow.
2371
2372 Returns None if no issue for this branch, or one of the following keywords:
2373 * 'error' - error from review tool (including deleted issues)
2374 * 'unsent' - no reviewers added
2375 * 'waiting' - waiting for review
2376 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002377 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002378 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002379 * 'commit' - in the commit queue
2380 * 'closed' - abandoned
2381 """
2382 if not self.GetIssue():
2383 return None
2384
2385 try:
2386 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002387 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002388 return 'error'
2389
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002390 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002391 return 'closed'
2392
2393 cq_label = data['labels'].get('Commit-Queue', {})
2394 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002395 votes = cq_label.get('all', [])
2396 highest_vote = 0
2397 for v in votes:
2398 highest_vote = max(highest_vote, v.get('value', 0))
2399 vote_value = str(highest_vote)
2400 if vote_value != '0':
2401 # Add a '+' if the value is not 0 to match the values in the label.
2402 # The cq_label does not have negatives.
2403 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002404 vote_text = cq_label.get('values', {}).get(vote_value, '')
2405 if vote_text.lower() == 'commit':
2406 return 'commit'
2407
2408 lgtm_label = data['labels'].get('Code-Review', {})
2409 if lgtm_label:
2410 if 'rejected' in lgtm_label:
2411 return 'not lgtm'
2412 if 'approved' in lgtm_label:
2413 return 'lgtm'
2414
2415 if not data.get('reviewers', {}).get('REVIEWER', []):
2416 return 'unsent'
2417
2418 messages = data.get('messages', [])
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002419 owner = data['owner'].get('_account_id')
2420 while messages:
2421 last_message_author = messages.pop().get('author', {})
2422 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2423 # Ignore replies from CQ.
2424 continue
Andrii Shyshkalov7afd1642017-01-29 16:07:15 +01002425 if last_message_author.get('_account_id') != owner:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002426 # Some reply from non-owner.
2427 return 'reply'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002428 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002429
2430 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002431 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002432 return data['revisions'][data['current_revision']]['_number']
2433
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002434 def FetchDescription(self, force=False):
2435 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2436 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002437 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002438 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002439
dsansomee2d6fd92016-09-08 00:10:47 -07002440 def UpdateDescriptionRemote(self, description, force=False):
2441 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2442 if not force:
2443 ask_for_data(
2444 'The description cannot be modified while the issue has a pending '
2445 'unpublished edit. Either publish the edit in the Gerrit web UI '
2446 'or delete it.\n\n'
2447 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2448
2449 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2450 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002451 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002452 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002453
2454 def CloseIssue(self):
2455 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2456
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002457 def GetApprovingReviewers(self):
2458 """Returns a list of reviewers approving the change.
2459
2460 Note: not necessarily committers.
2461 """
2462 raise NotImplementedError()
2463
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002464 def SubmitIssue(self, wait_for_merge=True):
2465 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2466 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002467
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002468 def _GetChangeDetail(self, options=None, issue=None,
2469 no_cache=False):
2470 """Returns details of the issue by querying Gerrit and caching results.
2471
2472 If fresh data is needed, set no_cache=True which will clear cache and
2473 thus new data will be fetched from Gerrit.
2474 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002475 options = options or []
2476 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002477 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002478
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002479 # Optimization to avoid multiple RPCs:
2480 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2481 'CURRENT_COMMIT' not in options):
2482 options.append('CURRENT_COMMIT')
2483
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002484 # Normalize issue and options for consistent keys in cache.
2485 issue = str(issue)
2486 options = [o.upper() for o in options]
2487
2488 # Check in cache first unless no_cache is True.
2489 if no_cache:
2490 self._detail_cache.pop(issue, None)
2491 else:
2492 options_set = frozenset(options)
2493 for cached_options_set, data in self._detail_cache.get(issue, []):
2494 # Assumption: data fetched before with extra options is suitable
2495 # for return for a smaller set of options.
2496 # For example, if we cached data for
2497 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2498 # and request is for options=[CURRENT_REVISION],
2499 # THEN we can return prior cached data.
2500 if options_set.issubset(cached_options_set):
2501 return data
2502
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002503 try:
2504 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2505 options, ignore_404=False)
2506 except gerrit_util.GerritError as e:
2507 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002508 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002509 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002510
2511 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002512 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002513
agable32978d92016-11-01 12:55:02 -07002514 def _GetChangeCommit(self, issue=None):
2515 issue = issue or self.GetIssue()
2516 assert issue, 'issue is required to query Gerrit'
2517 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2518 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002519 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002520 return data
2521
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002522 def CMDLand(self, force, bypass_hooks, verbose):
2523 if git_common.is_dirty_git_tree('land'):
2524 return 1
tandriid60367b2016-06-22 05:25:12 -07002525 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2526 if u'Commit-Queue' in detail.get('labels', {}):
2527 if not force:
2528 ask_for_data('\nIt seems this repository has a Commit Queue, '
2529 'which can test and land changes for you. '
2530 'Are you sure you wish to bypass it?\n'
2531 'Press Enter to continue, Ctrl+C to abort.')
2532
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002533 differs = True
tandriic4344b52016-08-29 06:04:54 -07002534 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002535 # Note: git diff outputs nothing if there is no diff.
2536 if not last_upload or RunGit(['diff', last_upload]).strip():
2537 print('WARNING: some changes from local branch haven\'t been uploaded')
2538 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002539 if detail['current_revision'] == last_upload:
2540 differs = False
2541 else:
2542 print('WARNING: local branch contents differ from latest uploaded '
2543 'patchset')
2544 if differs:
2545 if not force:
2546 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002547 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2548 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002549 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2550 elif not bypass_hooks:
2551 hook_results = self.RunHook(
2552 committing=True,
2553 may_prompt=not force,
2554 verbose=verbose,
2555 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2556 if not hook_results.should_continue():
2557 return 1
2558
2559 self.SubmitIssue(wait_for_merge=True)
2560 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002561 links = self._GetChangeCommit().get('web_links', [])
2562 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002563 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002564 print('Landed as %s' % link.get('url'))
2565 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002566 return 0
2567
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002568 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2569 directory):
2570 assert not reject
2571 assert not nocommit
2572 assert not directory
2573 assert parsed_issue_arg.valid
2574
2575 self._changelist.issue = parsed_issue_arg.issue
2576
2577 if parsed_issue_arg.hostname:
2578 self._gerrit_host = parsed_issue_arg.hostname
2579 self._gerrit_server = 'https://%s' % self._gerrit_host
2580
tandriic2405f52016-10-10 08:13:15 -07002581 try:
2582 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002583 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002584 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002585
2586 if not parsed_issue_arg.patchset:
2587 # Use current revision by default.
2588 revision_info = detail['revisions'][detail['current_revision']]
2589 patchset = int(revision_info['_number'])
2590 else:
2591 patchset = parsed_issue_arg.patchset
2592 for revision_info in detail['revisions'].itervalues():
2593 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2594 break
2595 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002596 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002597 (parsed_issue_arg.patchset, self.GetIssue()))
2598
2599 fetch_info = revision_info['fetch']['http']
2600 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2601 RunGit(['cherry-pick', 'FETCH_HEAD'])
2602 self.SetIssue(self.GetIssue())
2603 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002604 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002605 (self.GetIssue(), self.GetPatchset()))
2606 return 0
2607
2608 @staticmethod
2609 def ParseIssueURL(parsed_url):
2610 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2611 return None
2612 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2613 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2614 # Short urls like https://domain/<issue_number> can be used, but don't allow
2615 # specifying the patchset (you'd 404), but we allow that here.
2616 if parsed_url.path == '/':
2617 part = parsed_url.fragment
2618 else:
2619 part = parsed_url.path
2620 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2621 if match:
2622 return _ParsedIssueNumberArgument(
2623 issue=int(match.group(2)),
2624 patchset=int(match.group(4)) if match.group(4) else None,
2625 hostname=parsed_url.netloc)
2626 return None
2627
tandrii16e0b4e2016-06-07 10:34:28 -07002628 def _GerritCommitMsgHookCheck(self, offer_removal):
2629 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2630 if not os.path.exists(hook):
2631 return
2632 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2633 # custom developer made one.
2634 data = gclient_utils.FileRead(hook)
2635 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2636 return
2637 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002638 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002639 'and may interfere with it in subtle ways.\n'
2640 'We recommend you remove the commit-msg hook.')
2641 if offer_removal:
2642 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2643 if reply.lower().startswith('y'):
2644 gclient_utils.rm_file_or_tree(hook)
2645 print('Gerrit commit-msg hook removed.')
2646 else:
2647 print('OK, will keep Gerrit commit-msg hook in place.')
2648
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002649 def CMDUploadChange(self, options, args, change):
2650 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002651 if options.squash and options.no_squash:
2652 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002653
2654 if not options.squash and not options.no_squash:
2655 # Load default for user, repo, squash=true, in this order.
2656 options.squash = settings.GetSquashGerritUploads()
2657 elif options.no_squash:
2658 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002659
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002660 # We assume the remote called "origin" is the one we want.
2661 # It is probably not worthwhile to support different workflows.
2662 gerrit_remote = 'origin'
2663
2664 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002665 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002666
Aaron Gableb56ad332017-01-06 15:24:31 -08002667 # This may be None; default fallback value is determined in logic below.
2668 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002669 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002670
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002671 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002672 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002673 if self.GetIssue():
2674 # Try to get the message from a previous upload.
2675 message = self.GetDescription()
2676 if not message:
2677 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002678 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002679 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002680 if not title:
2681 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2682 title = ask_for_data(
2683 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002684 if title == default_title:
2685 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002686 change_id = self._GetChangeDetail()['change_id']
2687 while True:
2688 footer_change_ids = git_footers.get_footer_change_id(message)
2689 if footer_change_ids == [change_id]:
2690 break
2691 if not footer_change_ids:
2692 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002693 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002694 continue
2695 # There is already a valid footer but with different or several ids.
2696 # Doing this automatically is non-trivial as we don't want to lose
2697 # existing other footers, yet we want to append just 1 desired
2698 # Change-Id. Thus, just create a new footer, but let user verify the
2699 # new description.
2700 message = '%s\n\nChange-Id: %s' % (message, change_id)
2701 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002702 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002703 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002704 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002705 'Please, check the proposed correction to the description, '
2706 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2707 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2708 change_id))
2709 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2710 if not options.force:
2711 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002712 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002713 message = change_desc.description
2714 if not message:
2715 DieWithError("Description is empty. Aborting...")
2716 # Continue the while loop.
2717 # Sanity check of this code - we should end up with proper message
2718 # footer.
2719 assert [change_id] == git_footers.get_footer_change_id(message)
2720 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002721 else: # if not self.GetIssue()
2722 if options.message:
2723 message = options.message
2724 else:
2725 message = CreateDescriptionFromLog(args)
2726 if options.title:
2727 message = options.title + '\n\n' + message
2728 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002729 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002730 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002731 # On first upload, patchset title is always this string, while
2732 # --title flag gets converted to first line of message.
2733 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002734 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002735 if not change_desc.description:
2736 DieWithError("Description is empty. Aborting...")
2737 message = change_desc.description
2738 change_ids = git_footers.get_footer_change_id(message)
2739 if len(change_ids) > 1:
2740 DieWithError('too many Change-Id footers, at most 1 allowed.')
2741 if not change_ids:
2742 # Generate the Change-Id automatically.
2743 message = git_footers.add_footer_change_id(
2744 message, GenerateGerritChangeId(message))
2745 change_desc.set_description(message)
2746 change_ids = git_footers.get_footer_change_id(message)
2747 assert len(change_ids) == 1
2748 change_id = change_ids[0]
2749
2750 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2751 if remote is '.':
2752 # If our upstream branch is local, we base our squashed commit on its
2753 # squashed version.
2754 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2755 # Check the squashed hash of the parent.
2756 parent = RunGit(['config',
2757 'branch.%s.gerritsquashhash' % upstream_branch_name],
2758 error_ok=True).strip()
2759 # Verify that the upstream branch has been uploaded too, otherwise
2760 # Gerrit will create additional CLs when uploading.
2761 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2762 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002763 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002764 '\nUpload upstream branch %s first.\n'
2765 'It is likely that this branch has been rebased since its last '
2766 'upload, so you just need to upload it again.\n'
2767 '(If you uploaded it with --no-squash, then branch dependencies '
2768 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002769 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002770 else:
2771 parent = self.GetCommonAncestorWithUpstream()
2772
2773 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2774 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2775 '-m', message]).strip()
2776 else:
2777 change_desc = ChangeDescription(
2778 options.message or CreateDescriptionFromLog(args))
2779 if not change_desc.description:
2780 DieWithError("Description is empty. Aborting...")
2781
2782 if not git_footers.get_footer_change_id(change_desc.description):
2783 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002784 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2785 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002786 ref_to_push = 'HEAD'
2787 parent = '%s/%s' % (gerrit_remote, branch)
2788 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2789
2790 assert change_desc
2791 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2792 ref_to_push)]).splitlines()
2793 if len(commits) > 1:
2794 print('WARNING: This will upload %d commits. Run the following command '
2795 'to see which commits will be uploaded: ' % len(commits))
2796 print('git log %s..%s' % (parent, ref_to_push))
2797 print('You can also use `git squash-branch` to squash these into a '
2798 'single commit.')
2799 ask_for_data('About to upload; enter to confirm.')
2800
2801 if options.reviewers or options.tbr_owners:
2802 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2803 change)
2804
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002805 # Extra options that can be specified at push time. Doc:
2806 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2807 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002808 if change_desc.get_reviewers(tbr_only=True):
2809 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2810 refspec_opts.append('l=Code-Review+1')
2811
Aaron Gable9b713dd2016-12-14 16:04:21 -08002812 if title:
2813 if not re.match(r'^[\w ]+$', title):
2814 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002815 if not automatic_title:
2816 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gableeb3af712017-02-02 12:42:02 -08002817 'and spaces. You can edit it in the UI. '
2818 'See https://crbug.com/663787.\n'
2819 'Cleaned up title: %s' % title)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002820 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2821 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002822 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002823
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002824 if options.send_mail:
2825 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002826 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002827 refspec_opts.append('notify=ALL')
2828 else:
2829 refspec_opts.append('notify=NONE')
2830
tandrii99a72f22016-08-17 14:33:24 -07002831 reviewers = change_desc.get_reviewers()
2832 if reviewers:
Andrii Shyshkalovaf3a9992017-02-09 14:18:01 +01002833 # TODO(tandrii): remove this horrible hack once (Poly)Gerrit fixes their
2834 # side for real (b/34702620).
2835 def clean_invisible_chars(email):
2836 return email.decode('unicode_escape').encode('ascii', 'ignore')
2837 refspec_opts.extend('r=' + clean_invisible_chars(email).strip()
2838 for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002839
agablec6787972016-09-09 16:13:34 -07002840 if options.private:
2841 refspec_opts.append('draft')
2842
rmistry9eadede2016-09-19 11:22:43 -07002843 if options.topic:
2844 # Documentation on Gerrit topics is here:
2845 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2846 refspec_opts.append('topic=%s' % options.topic)
2847
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002848 refspec_suffix = ''
2849 if refspec_opts:
2850 refspec_suffix = '%' + ','.join(refspec_opts)
2851 assert ' ' not in refspec_suffix, (
2852 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002853 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002854
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002855 try:
2856 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalove9c78ff2017-02-06 15:53:13 +01002857 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002858 print_stdout=True,
2859 # Flush after every line: useful for seeing progress when running as
2860 # recipe.
2861 filter_fn=lambda _: sys.stdout.flush())
2862 except subprocess2.CalledProcessError:
2863 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002864 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002865
2866 if options.squash:
2867 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2868 change_numbers = [m.group(1)
2869 for m in map(regex.match, push_stdout.splitlines())
2870 if m]
2871 if len(change_numbers) != 1:
2872 DieWithError(
2873 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002874 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002875 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002876 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002877
2878 # Add cc's from the CC_LIST and --cc flag (if any).
2879 cc = self.GetCCList().split(',')
2880 if options.cc:
2881 cc.extend(options.cc)
2882 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002883 if change_desc.get_cced():
2884 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002885 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002886 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002887 self._GetGerritHost(), self.GetIssue(), cc,
2888 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002889 return 0
2890
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002891 def _AddChangeIdToCommitMessage(self, options, args):
2892 """Re-commits using the current message, assumes the commit hook is in
2893 place.
2894 """
2895 log_desc = options.message or CreateDescriptionFromLog(args)
2896 git_command = ['commit', '--amend', '-m', log_desc]
2897 RunGit(git_command)
2898 new_log_desc = CreateDescriptionFromLog(args)
2899 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002900 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002901 return new_log_desc
2902 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002903 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002904
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002905 def SetCQState(self, new_state):
2906 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002907 vote_map = {
2908 _CQState.NONE: 0,
2909 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002910 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002911 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002912 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2913 if new_state == _CQState.DRY_RUN:
2914 # Don't spam everybody reviewer/owner.
2915 kwargs['notify'] = 'NONE'
2916 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002917
tandriie113dfd2016-10-11 10:20:12 -07002918 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002919 try:
2920 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002921 except GerritChangeNotExists:
2922 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002923
2924 if data['status'] in ('ABANDONED', 'MERGED'):
2925 return 'CL %s is closed' % self.GetIssue()
2926
2927 def GetTryjobProperties(self, patchset=None):
2928 """Returns dictionary of properties to launch tryjob."""
2929 data = self._GetChangeDetail(['ALL_REVISIONS'])
2930 patchset = int(patchset or self.GetPatchset())
2931 assert patchset
2932 revision_data = None # Pylint wants it to be defined.
2933 for revision_data in data['revisions'].itervalues():
2934 if int(revision_data['_number']) == patchset:
2935 break
2936 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002937 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002938 (patchset, self.GetIssue()))
2939 return {
2940 'patch_issue': self.GetIssue(),
2941 'patch_set': patchset or self.GetPatchset(),
2942 'patch_project': data['project'],
2943 'patch_storage': 'gerrit',
2944 'patch_ref': revision_data['fetch']['http']['ref'],
2945 'patch_repository_url': revision_data['fetch']['http']['url'],
2946 'patch_gerrit_url': self.GetCodereviewServer(),
2947 }
tandriie113dfd2016-10-11 10:20:12 -07002948
tandriide281ae2016-10-12 06:02:30 -07002949 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002950 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002951
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002952
2953_CODEREVIEW_IMPLEMENTATIONS = {
2954 'rietveld': _RietveldChangelistImpl,
2955 'gerrit': _GerritChangelistImpl,
2956}
2957
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002958
iannuccie53c9352016-08-17 14:40:40 -07002959def _add_codereview_issue_select_options(parser, extra=""):
2960 _add_codereview_select_options(parser)
2961
2962 text = ('Operate on this issue number instead of the current branch\'s '
2963 'implicit issue.')
2964 if extra:
2965 text += ' '+extra
2966 parser.add_option('-i', '--issue', type=int, help=text)
2967
2968
2969def _process_codereview_issue_select_options(parser, options):
2970 _process_codereview_select_options(parser, options)
2971 if options.issue is not None and not options.forced_codereview:
2972 parser.error('--issue must be specified with either --rietveld or --gerrit')
2973
2974
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002975def _add_codereview_select_options(parser):
2976 """Appends --gerrit and --rietveld options to force specific codereview."""
2977 parser.codereview_group = optparse.OptionGroup(
2978 parser, 'EXPERIMENTAL! Codereview override options')
2979 parser.add_option_group(parser.codereview_group)
2980 parser.codereview_group.add_option(
2981 '--gerrit', action='store_true',
2982 help='Force the use of Gerrit for codereview')
2983 parser.codereview_group.add_option(
2984 '--rietveld', action='store_true',
2985 help='Force the use of Rietveld for codereview')
2986
2987
2988def _process_codereview_select_options(parser, options):
2989 if options.gerrit and options.rietveld:
2990 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2991 options.forced_codereview = None
2992 if options.gerrit:
2993 options.forced_codereview = 'gerrit'
2994 elif options.rietveld:
2995 options.forced_codereview = 'rietveld'
2996
2997
tandriif9aefb72016-07-01 09:06:51 -07002998def _get_bug_line_values(default_project, bugs):
2999 """Given default_project and comma separated list of bugs, yields bug line
3000 values.
3001
3002 Each bug can be either:
3003 * a number, which is combined with default_project
3004 * string, which is left as is.
3005
3006 This function may produce more than one line, because bugdroid expects one
3007 project per line.
3008
3009 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3010 ['v8:123', 'chromium:789']
3011 """
3012 default_bugs = []
3013 others = []
3014 for bug in bugs.split(','):
3015 bug = bug.strip()
3016 if bug:
3017 try:
3018 default_bugs.append(int(bug))
3019 except ValueError:
3020 others.append(bug)
3021
3022 if default_bugs:
3023 default_bugs = ','.join(map(str, default_bugs))
3024 if default_project:
3025 yield '%s:%s' % (default_project, default_bugs)
3026 else:
3027 yield default_bugs
3028 for other in sorted(others):
3029 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3030 yield other
3031
3032
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003033class ChangeDescription(object):
3034 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003035 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003036 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003037 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003038 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003039
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003040 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003041 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003042
agable@chromium.org42c20792013-09-12 17:34:49 +00003043 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003044 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003045 return '\n'.join(self._description_lines)
3046
3047 def set_description(self, desc):
3048 if isinstance(desc, basestring):
3049 lines = desc.splitlines()
3050 else:
3051 lines = [line.rstrip() for line in desc]
3052 while lines and not lines[0]:
3053 lines.pop(0)
3054 while lines and not lines[-1]:
3055 lines.pop(-1)
3056 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003057
piman@chromium.org336f9122014-09-04 02:16:55 +00003058 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003059 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003060 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003061 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003062 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003063 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003064
agable@chromium.org42c20792013-09-12 17:34:49 +00003065 # Get the set of R= and TBR= lines and remove them from the desciption.
3066 regexp = re.compile(self.R_LINE)
3067 matches = [regexp.match(line) for line in self._description_lines]
3068 new_desc = [l for i, l in enumerate(self._description_lines)
3069 if not matches[i]]
3070 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003071
agable@chromium.org42c20792013-09-12 17:34:49 +00003072 # Construct new unified R= and TBR= lines.
3073 r_names = []
3074 tbr_names = []
3075 for match in matches:
3076 if not match:
3077 continue
3078 people = cleanup_list([match.group(2).strip()])
3079 if match.group(1) == 'TBR':
3080 tbr_names.extend(people)
3081 else:
3082 r_names.extend(people)
3083 for name in r_names:
3084 if name not in reviewers:
3085 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003086 if add_owners_tbr:
3087 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003088 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003089 all_reviewers = set(tbr_names + reviewers)
3090 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3091 all_reviewers)
3092 tbr_names.extend(owners_db.reviewers_for(missing_files,
3093 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003094 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3095 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3096
3097 # Put the new lines in the description where the old first R= line was.
3098 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3099 if 0 <= line_loc < len(self._description_lines):
3100 if new_tbr_line:
3101 self._description_lines.insert(line_loc, new_tbr_line)
3102 if new_r_line:
3103 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003104 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003105 if new_r_line:
3106 self.append_footer(new_r_line)
3107 if new_tbr_line:
3108 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003109
tandriif9aefb72016-07-01 09:06:51 -07003110 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003111 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003112 self.set_description([
3113 '# Enter a description of the change.',
3114 '# This will be displayed on the codereview site.',
3115 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003116 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003117 '--------------------',
3118 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003119
agable@chromium.org42c20792013-09-12 17:34:49 +00003120 regexp = re.compile(self.BUG_LINE)
3121 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003122 prefix = settings.GetBugPrefix()
3123 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3124 for value in values:
3125 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3126 self.append_footer('BUG=%s' % value)
3127
agable@chromium.org42c20792013-09-12 17:34:49 +00003128 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003129 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003130 if not content:
3131 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003132 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003133
3134 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003135 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3136 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003137 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003138 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003139
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003140 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003141 """Adds a footer line to the description.
3142
3143 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3144 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3145 that Gerrit footers are always at the end.
3146 """
3147 parsed_footer_line = git_footers.parse_footer(line)
3148 if parsed_footer_line:
3149 # Line is a gerrit footer in the form: Footer-Key: any value.
3150 # Thus, must be appended observing Gerrit footer rules.
3151 self.set_description(
3152 git_footers.add_footer(self.description,
3153 key=parsed_footer_line[0],
3154 value=parsed_footer_line[1]))
3155 return
3156
3157 if not self._description_lines:
3158 self._description_lines.append(line)
3159 return
3160
3161 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3162 if gerrit_footers:
3163 # git_footers.split_footers ensures that there is an empty line before
3164 # actual (gerrit) footers, if any. We have to keep it that way.
3165 assert top_lines and top_lines[-1] == ''
3166 top_lines, separator = top_lines[:-1], top_lines[-1:]
3167 else:
3168 separator = [] # No need for separator if there are no gerrit_footers.
3169
3170 prev_line = top_lines[-1] if top_lines else ''
3171 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3172 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3173 top_lines.append('')
3174 top_lines.append(line)
3175 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003176
tandrii99a72f22016-08-17 14:33:24 -07003177 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003178 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003179 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003180 reviewers = [match.group(2).strip()
3181 for match in matches
3182 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003183 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003184
bradnelsond975b302016-10-23 12:20:23 -07003185 def get_cced(self):
3186 """Retrieves the list of reviewers."""
3187 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3188 cced = [match.group(2).strip() for match in matches if match]
3189 return cleanup_list(cced)
3190
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003191 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3192 """Updates this commit description given the parent.
3193
3194 This is essentially what Gnumbd used to do.
3195 Consult https://goo.gl/WMmpDe for more details.
3196 """
3197 assert parent_msg # No, orphan branch creation isn't supported.
3198 assert parent_hash
3199 assert dest_ref
3200 parent_footer_map = git_footers.parse_footers(parent_msg)
3201 # This will also happily parse svn-position, which GnumbD is no longer
3202 # supporting. While we'd generate correct footers, the verifier plugin
3203 # installed in Gerrit will block such commit (ie git push below will fail).
3204 parent_position = git_footers.get_position(parent_footer_map)
3205
3206 # Cherry-picks may have last line obscuring their prior footers,
3207 # from git_footers perspective. This is also what Gnumbd did.
3208 cp_line = None
3209 if (self._description_lines and
3210 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3211 cp_line = self._description_lines.pop()
3212
3213 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3214
3215 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3216 # user interference with actual footers we'd insert below.
3217 for i, (k, v) in enumerate(parsed_footers):
3218 if k.startswith('Cr-'):
3219 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3220
3221 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003222 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003223 if parent_position[0] == dest_ref:
3224 # Same branch as parent.
3225 number = int(parent_position[1]) + 1
3226 else:
3227 number = 1 # New branch, and extra lineage.
3228 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3229 int(parent_position[1])))
3230
3231 parsed_footers.append(('Cr-Commit-Position',
3232 '%s@{#%d}' % (dest_ref, number)))
3233 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3234
3235 self._description_lines = top_lines
3236 if cp_line:
3237 self._description_lines.append(cp_line)
3238 if self._description_lines[-1] != '':
3239 self._description_lines.append('') # Ensure footer separator.
3240 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3241
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003242
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003243def get_approving_reviewers(props):
3244 """Retrieves the reviewers that approved a CL from the issue properties with
3245 messages.
3246
3247 Note that the list may contain reviewers that are not committer, thus are not
3248 considered by the CQ.
3249 """
3250 return sorted(
3251 set(
3252 message['sender']
3253 for message in props['messages']
3254 if message['approval'] and message['sender'] in props['reviewers']
3255 )
3256 )
3257
3258
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003259def FindCodereviewSettingsFile(filename='codereview.settings'):
3260 """Finds the given file starting in the cwd and going up.
3261
3262 Only looks up to the top of the repository unless an
3263 'inherit-review-settings-ok' file exists in the root of the repository.
3264 """
3265 inherit_ok_file = 'inherit-review-settings-ok'
3266 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003267 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003268 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3269 root = '/'
3270 while True:
3271 if filename in os.listdir(cwd):
3272 if os.path.isfile(os.path.join(cwd, filename)):
3273 return open(os.path.join(cwd, filename))
3274 if cwd == root:
3275 break
3276 cwd = os.path.dirname(cwd)
3277
3278
3279def LoadCodereviewSettingsFromFile(fileobj):
3280 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003281 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003282
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003283 def SetProperty(name, setting, unset_error_ok=False):
3284 fullname = 'rietveld.' + name
3285 if setting in keyvals:
3286 RunGit(['config', fullname, keyvals[setting]])
3287 else:
3288 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3289
tandrii48df5812016-10-17 03:55:37 -07003290 if not keyvals.get('GERRIT_HOST', False):
3291 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003292 # Only server setting is required. Other settings can be absent.
3293 # In that case, we ignore errors raised during option deletion attempt.
3294 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003295 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003296 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3297 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003298 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003299 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3300 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003301 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003302 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3303 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003304
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003305 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003306 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003307
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003308 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003309 RunGit(['config', 'gerrit.squash-uploads',
3310 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003311
tandrii@chromium.org28253532016-04-14 13:46:56 +00003312 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003313 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003314 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3315
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003316 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003317 # should be of the form
3318 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3319 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003320 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3321 keyvals['ORIGIN_URL_CONFIG']])
3322
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003323
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003324def urlretrieve(source, destination):
3325 """urllib is broken for SSL connections via a proxy therefore we
3326 can't use urllib.urlretrieve()."""
3327 with open(destination, 'w') as f:
3328 f.write(urllib2.urlopen(source).read())
3329
3330
ukai@chromium.org712d6102013-11-27 00:52:58 +00003331def hasSheBang(fname):
3332 """Checks fname is a #! script."""
3333 with open(fname) as f:
3334 return f.read(2).startswith('#!')
3335
3336
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003337# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3338def DownloadHooks(*args, **kwargs):
3339 pass
3340
3341
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003342def DownloadGerritHook(force):
3343 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003344
3345 Args:
3346 force: True to update hooks. False to install hooks if not present.
3347 """
3348 if not settings.GetIsGerrit():
3349 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003350 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003351 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3352 if not os.access(dst, os.X_OK):
3353 if os.path.exists(dst):
3354 if not force:
3355 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003356 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003357 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003358 if not hasSheBang(dst):
3359 DieWithError('Not a script: %s\n'
3360 'You need to download from\n%s\n'
3361 'into .git/hooks/commit-msg and '
3362 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003363 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3364 except Exception:
3365 if os.path.exists(dst):
3366 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003367 DieWithError('\nFailed to download hooks.\n'
3368 'You need to download from\n%s\n'
3369 'into .git/hooks/commit-msg and '
3370 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003371
3372
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003373def GetRietveldCodereviewSettingsInteractively():
3374 """Prompt the user for settings."""
3375 server = settings.GetDefaultServerUrl(error_ok=True)
3376 prompt = 'Rietveld server (host[:port])'
3377 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3378 newserver = ask_for_data(prompt + ':')
3379 if not server and not newserver:
3380 newserver = DEFAULT_SERVER
3381 if newserver:
3382 newserver = gclient_utils.UpgradeToHttps(newserver)
3383 if newserver != server:
3384 RunGit(['config', 'rietveld.server', newserver])
3385
3386 def SetProperty(initial, caption, name, is_url):
3387 prompt = caption
3388 if initial:
3389 prompt += ' ("x" to clear) [%s]' % initial
3390 new_val = ask_for_data(prompt + ':')
3391 if new_val == 'x':
3392 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3393 elif new_val:
3394 if is_url:
3395 new_val = gclient_utils.UpgradeToHttps(new_val)
3396 if new_val != initial:
3397 RunGit(['config', 'rietveld.' + name, new_val])
3398
3399 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3400 SetProperty(settings.GetDefaultPrivateFlag(),
3401 'Private flag (rietveld only)', 'private', False)
3402 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3403 'tree-status-url', False)
3404 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3405 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3406 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3407 'run-post-upload-hook', False)
3408
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003409
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003410@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003411def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003412 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003413
tandrii5d0a0422016-09-14 06:24:35 -07003414 print('WARNING: git cl config works for Rietveld only')
3415 # TODO(tandrii): remove this once we switch to Gerrit.
3416 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003417 parser.add_option('--activate-update', action='store_true',
3418 help='activate auto-updating [rietveld] section in '
3419 '.git/config')
3420 parser.add_option('--deactivate-update', action='store_true',
3421 help='deactivate auto-updating [rietveld] section in '
3422 '.git/config')
3423 options, args = parser.parse_args(args)
3424
3425 if options.deactivate_update:
3426 RunGit(['config', 'rietveld.autoupdate', 'false'])
3427 return
3428
3429 if options.activate_update:
3430 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3431 return
3432
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003433 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003434 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003435 return 0
3436
3437 url = args[0]
3438 if not url.endswith('codereview.settings'):
3439 url = os.path.join(url, 'codereview.settings')
3440
3441 # Load code review settings and download hooks (if available).
3442 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3443 return 0
3444
3445
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003446def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003447 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003448 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3449 branch = ShortBranchName(branchref)
3450 _, args = parser.parse_args(args)
3451 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003452 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003453 return RunGit(['config', 'branch.%s.base-url' % branch],
3454 error_ok=False).strip()
3455 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003456 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003457 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3458 error_ok=False).strip()
3459
3460
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003461def color_for_status(status):
3462 """Maps a Changelist status to color, for CMDstatus and other tools."""
3463 return {
3464 'unsent': Fore.RED,
3465 'waiting': Fore.BLUE,
3466 'reply': Fore.YELLOW,
3467 'lgtm': Fore.GREEN,
3468 'commit': Fore.MAGENTA,
3469 'closed': Fore.CYAN,
3470 'error': Fore.WHITE,
3471 }.get(status, Fore.WHITE)
3472
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003473
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003474def get_cl_statuses(changes, fine_grained, max_processes=None):
3475 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003476
3477 If fine_grained is true, this will fetch CL statuses from the server.
3478 Otherwise, simply indicate if there's a matching url for the given branches.
3479
3480 If max_processes is specified, it is used as the maximum number of processes
3481 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3482 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003483
3484 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003485 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003486 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003487 upload.verbosity = 0
3488
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003489 if not changes:
3490 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003491
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003492 if not fine_grained:
3493 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003494 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003495 for cl in changes:
3496 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003497 return
3498
3499 # First, sort out authentication issues.
3500 logging.debug('ensuring credentials exist')
3501 for cl in changes:
3502 cl.EnsureAuthenticated(force=False, refresh=True)
3503
3504 def fetch(cl):
3505 try:
3506 return (cl, cl.GetStatus())
3507 except:
3508 # See http://crbug.com/629863.
3509 logging.exception('failed to fetch status for %s:', cl)
3510 raise
3511
3512 threads_count = len(changes)
3513 if max_processes:
3514 threads_count = max(1, min(threads_count, max_processes))
3515 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3516
3517 pool = ThreadPool(threads_count)
3518 fetched_cls = set()
3519 try:
3520 it = pool.imap_unordered(fetch, changes).__iter__()
3521 while True:
3522 try:
3523 cl, status = it.next(timeout=5)
3524 except multiprocessing.TimeoutError:
3525 break
3526 fetched_cls.add(cl)
3527 yield cl, status
3528 finally:
3529 pool.close()
3530
3531 # Add any branches that failed to fetch.
3532 for cl in set(changes) - fetched_cls:
3533 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003534
rmistry@google.com2dd99862015-06-22 12:22:18 +00003535
3536def upload_branch_deps(cl, args):
3537 """Uploads CLs of local branches that are dependents of the current branch.
3538
3539 If the local branch dependency tree looks like:
3540 test1 -> test2.1 -> test3.1
3541 -> test3.2
3542 -> test2.2 -> test3.3
3543
3544 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3545 run on the dependent branches in this order:
3546 test2.1, test3.1, test3.2, test2.2, test3.3
3547
3548 Note: This function does not rebase your local dependent branches. Use it when
3549 you make a change to the parent branch that will not conflict with its
3550 dependent branches, and you would like their dependencies updated in
3551 Rietveld.
3552 """
3553 if git_common.is_dirty_git_tree('upload-branch-deps'):
3554 return 1
3555
3556 root_branch = cl.GetBranch()
3557 if root_branch is None:
3558 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3559 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003560 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003561 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3562 'patchset dependencies without an uploaded CL.')
3563
3564 branches = RunGit(['for-each-ref',
3565 '--format=%(refname:short) %(upstream:short)',
3566 'refs/heads'])
3567 if not branches:
3568 print('No local branches found.')
3569 return 0
3570
3571 # Create a dictionary of all local branches to the branches that are dependent
3572 # on it.
3573 tracked_to_dependents = collections.defaultdict(list)
3574 for b in branches.splitlines():
3575 tokens = b.split()
3576 if len(tokens) == 2:
3577 branch_name, tracked = tokens
3578 tracked_to_dependents[tracked].append(branch_name)
3579
vapiera7fbd5a2016-06-16 09:17:49 -07003580 print()
3581 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003582 dependents = []
3583 def traverse_dependents_preorder(branch, padding=''):
3584 dependents_to_process = tracked_to_dependents.get(branch, [])
3585 padding += ' '
3586 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003587 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003588 dependents.append(dependent)
3589 traverse_dependents_preorder(dependent, padding)
3590 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003591 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003592
3593 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003595 return 0
3596
vapiera7fbd5a2016-06-16 09:17:49 -07003597 print('This command will checkout all dependent branches and run '
3598 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003599 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3600
andybons@chromium.org962f9462016-02-03 20:00:42 +00003601 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003602 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003603 args.extend(['-t', 'Updated patchset dependency'])
3604
rmistry@google.com2dd99862015-06-22 12:22:18 +00003605 # Record all dependents that failed to upload.
3606 failures = {}
3607 # Go through all dependents, checkout the branch and upload.
3608 try:
3609 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003610 print()
3611 print('--------------------------------------')
3612 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003613 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003614 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003615 try:
3616 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003617 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003618 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003619 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003620 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003621 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003622 finally:
3623 # Swap back to the original root branch.
3624 RunGit(['checkout', '-q', root_branch])
3625
vapiera7fbd5a2016-06-16 09:17:49 -07003626 print()
3627 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003628 for dependent_branch in dependents:
3629 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003630 print(' %s : %s' % (dependent_branch, upload_status))
3631 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003632
3633 return 0
3634
3635
kmarshall3bff56b2016-06-06 18:31:47 -07003636def CMDarchive(parser, args):
3637 """Archives and deletes branches associated with closed changelists."""
3638 parser.add_option(
3639 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003640 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003641 parser.add_option(
3642 '-f', '--force', action='store_true',
3643 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003644 parser.add_option(
3645 '-d', '--dry-run', action='store_true',
3646 help='Skip the branch tagging and removal steps.')
3647 parser.add_option(
3648 '-t', '--notags', action='store_true',
3649 help='Do not tag archived branches. '
3650 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003651
3652 auth.add_auth_options(parser)
3653 options, args = parser.parse_args(args)
3654 if args:
3655 parser.error('Unsupported args: %s' % ' '.join(args))
3656 auth_config = auth.extract_auth_config_from_options(options)
3657
3658 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3659 if not branches:
3660 return 0
3661
vapiera7fbd5a2016-06-16 09:17:49 -07003662 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003663 changes = [Changelist(branchref=b, auth_config=auth_config)
3664 for b in branches.splitlines()]
3665 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3666 statuses = get_cl_statuses(changes,
3667 fine_grained=True,
3668 max_processes=options.maxjobs)
3669 proposal = [(cl.GetBranch(),
3670 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3671 for cl, status in statuses
3672 if status == 'closed']
3673 proposal.sort()
3674
3675 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003676 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003677 return 0
3678
3679 current_branch = GetCurrentBranch()
3680
vapiera7fbd5a2016-06-16 09:17:49 -07003681 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003682 if options.notags:
3683 for next_item in proposal:
3684 print(' ' + next_item[0])
3685 else:
3686 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3687 for next_item in proposal:
3688 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003689
kmarshall9249e012016-08-23 12:02:16 -07003690 # Quit now on precondition failure or if instructed by the user, either
3691 # via an interactive prompt or by command line flags.
3692 if options.dry_run:
3693 print('\nNo changes were made (dry run).\n')
3694 return 0
3695 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003696 print('You are currently on a branch \'%s\' which is associated with a '
3697 'closed codereview issue, so archive cannot proceed. Please '
3698 'checkout another branch and run this command again.' %
3699 current_branch)
3700 return 1
kmarshall9249e012016-08-23 12:02:16 -07003701 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003702 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3703 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003704 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003705 return 1
3706
3707 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003708 if not options.notags:
3709 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003710 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003711
vapiera7fbd5a2016-06-16 09:17:49 -07003712 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003713
3714 return 0
3715
3716
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003717def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003718 """Show status of changelists.
3719
3720 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003721 - Red not sent for review or broken
3722 - Blue waiting for review
3723 - Yellow waiting for you to reply to review
3724 - Green LGTM'ed
3725 - Magenta in the commit queue
3726 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003727
3728 Also see 'git cl comments'.
3729 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003730 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003731 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003732 parser.add_option('-f', '--fast', action='store_true',
3733 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003734 parser.add_option(
3735 '-j', '--maxjobs', action='store', type=int,
3736 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003737
3738 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003739 _add_codereview_issue_select_options(
3740 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003741 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003742 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003743 if args:
3744 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003745 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003746
iannuccie53c9352016-08-17 14:40:40 -07003747 if options.issue is not None and not options.field:
3748 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003749
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003750 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003751 cl = Changelist(auth_config=auth_config, issue=options.issue,
3752 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003753 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003754 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003755 elif options.field == 'id':
3756 issueid = cl.GetIssue()
3757 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003758 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003759 elif options.field == 'patch':
3760 patchset = cl.GetPatchset()
3761 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003762 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003763 elif options.field == 'status':
3764 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003765 elif options.field == 'url':
3766 url = cl.GetIssueURL()
3767 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003768 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003769 return 0
3770
3771 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3772 if not branches:
3773 print('No local branch found.')
3774 return 0
3775
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003776 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003777 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003778 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003779 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003780 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003781 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003782 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003783
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003784 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003785 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3786 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3787 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003788 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003789 c, status = output.next()
3790 branch_statuses[c.GetBranch()] = status
3791 status = branch_statuses.pop(branch)
3792 url = cl.GetIssueURL()
3793 if url and (not status or status == 'error'):
3794 # The issue probably doesn't exist anymore.
3795 url += ' (broken)'
3796
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003797 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003798 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003799 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003800 color = ''
3801 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003802 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003803 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003804 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003805 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003806
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003807
3808 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003809 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003810 print('Current branch: %s' % branch)
3811 for cl in changes:
3812 if cl.GetBranch() == branch:
3813 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003814 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003815 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003816 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003817 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003818 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003819 print('Issue description:')
3820 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003821 return 0
3822
3823
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003824def colorize_CMDstatus_doc():
3825 """To be called once in main() to add colors to git cl status help."""
3826 colors = [i for i in dir(Fore) if i[0].isupper()]
3827
3828 def colorize_line(line):
3829 for color in colors:
3830 if color in line.upper():
3831 # Extract whitespaces first and the leading '-'.
3832 indent = len(line) - len(line.lstrip(' ')) + 1
3833 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3834 return line
3835
3836 lines = CMDstatus.__doc__.splitlines()
3837 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3838
3839
phajdan.jre328cf92016-08-22 04:12:17 -07003840def write_json(path, contents):
3841 with open(path, 'w') as f:
3842 json.dump(contents, f)
3843
3844
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003845@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003846def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003847 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003848
3849 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003850 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003851 parser.add_option('-r', '--reverse', action='store_true',
3852 help='Lookup the branch(es) for the specified issues. If '
3853 'no issues are specified, all branches with mapped '
3854 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003855 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003856 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003857 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003858 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003859
dnj@chromium.org406c4402015-03-03 17:22:28 +00003860 if options.reverse:
3861 branches = RunGit(['for-each-ref', 'refs/heads',
3862 '--format=%(refname:short)']).splitlines()
3863
3864 # Reverse issue lookup.
3865 issue_branch_map = {}
3866 for branch in branches:
3867 cl = Changelist(branchref=branch)
3868 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3869 if not args:
3870 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003871 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003872 for issue in args:
3873 if not issue:
3874 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003875 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003876 print('Branch for issue number %s: %s' % (
3877 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003878 if options.json:
3879 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003880 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003881 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003882 if len(args) > 0:
3883 try:
3884 issue = int(args[0])
3885 except ValueError:
3886 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003887 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003888 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003889 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003890 if options.json:
3891 write_json(options.json, {
3892 'issue': cl.GetIssue(),
3893 'issue_url': cl.GetIssueURL(),
3894 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003895 return 0
3896
3897
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003898def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003899 """Shows or posts review comments for any changelist."""
3900 parser.add_option('-a', '--add-comment', dest='comment',
3901 help='comment to add to an issue')
3902 parser.add_option('-i', dest='issue',
3903 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003904 parser.add_option('-j', '--json-file',
3905 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003906 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003907 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003908 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003909
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003910 issue = None
3911 if options.issue:
3912 try:
3913 issue = int(options.issue)
3914 except ValueError:
3915 DieWithError('A review issue id is expected to be a number')
3916
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003917 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003918
3919 if options.comment:
3920 cl.AddComment(options.comment)
3921 return 0
3922
3923 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003924 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003925 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003926 summary.append({
3927 'date': message['date'],
3928 'lgtm': False,
3929 'message': message['text'],
3930 'not_lgtm': False,
3931 'sender': message['sender'],
3932 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003933 if message['disapproval']:
3934 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003935 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003936 elif message['approval']:
3937 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003938 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003939 elif message['sender'] == data['owner_email']:
3940 color = Fore.MAGENTA
3941 else:
3942 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003943 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003944 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003945 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003946 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003947 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003948 if options.json_file:
3949 with open(options.json_file, 'wb') as f:
3950 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003951 return 0
3952
3953
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003954@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003955def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003956 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003957 parser.add_option('-d', '--display', action='store_true',
3958 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003959 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003960 help='New description to set for this issue (- for stdin, '
3961 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003962 parser.add_option('-f', '--force', action='store_true',
3963 help='Delete any unpublished Gerrit edits for this issue '
3964 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003965
3966 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003967 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003968 options, args = parser.parse_args(args)
3969 _process_codereview_select_options(parser, options)
3970
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003971 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003972 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003973 target_issue_arg = ParseIssueNumberArgument(args[0])
3974 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003975 parser.print_help()
3976 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003977
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003978 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003979
martiniss6eda05f2016-06-30 10:18:35 -07003980 kwargs = {
3981 'auth_config': auth_config,
3982 'codereview': options.forced_codereview,
3983 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003984 if target_issue_arg:
3985 kwargs['issue'] = target_issue_arg.issue
3986 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07003987
3988 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003989
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003990 if not cl.GetIssue():
3991 DieWithError('This branch has no associated changelist.')
3992 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003993
smut@google.com34fb6b12015-07-13 20:03:26 +00003994 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003995 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003996 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003997
3998 if options.new_description:
3999 text = options.new_description
4000 if text == '-':
4001 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004002 elif text == '+':
4003 base_branch = cl.GetCommonAncestorWithUpstream()
4004 change = cl.GetChange(base_branch, None, local_description=True)
4005 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004006
4007 description.set_description(text)
4008 else:
4009 description.prompt()
4010
wychen@chromium.org063e4e52015-04-03 06:51:44 +00004011 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004012 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004013 return 0
4014
4015
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004016def CreateDescriptionFromLog(args):
4017 """Pulls out the commit log to use as a base for the CL description."""
4018 log_args = []
4019 if len(args) == 1 and not args[0].endswith('.'):
4020 log_args = [args[0] + '..']
4021 elif len(args) == 1 and args[0].endswith('...'):
4022 log_args = [args[0][:-1]]
4023 elif len(args) == 2:
4024 log_args = [args[0] + '..' + args[1]]
4025 else:
4026 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004027 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004028
4029
thestig@chromium.org44202a22014-03-11 19:22:18 +00004030def CMDlint(parser, args):
4031 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004032 parser.add_option('--filter', action='append', metavar='-x,+y',
4033 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004034 auth.add_auth_options(parser)
4035 options, args = parser.parse_args(args)
4036 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004037
4038 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004039 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004040 try:
4041 import cpplint
4042 import cpplint_chromium
4043 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004044 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004045 return 1
4046
4047 # Change the current working directory before calling lint so that it
4048 # shows the correct base.
4049 previous_cwd = os.getcwd()
4050 os.chdir(settings.GetRoot())
4051 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004052 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004053 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4054 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004055 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004056 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004057 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004058
4059 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004060 command = args + files
4061 if options.filter:
4062 command = ['--filter=' + ','.join(options.filter)] + command
4063 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004064
4065 white_regex = re.compile(settings.GetLintRegex())
4066 black_regex = re.compile(settings.GetLintIgnoreRegex())
4067 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4068 for filename in filenames:
4069 if white_regex.match(filename):
4070 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004071 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004072 else:
4073 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4074 extra_check_functions)
4075 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004076 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004077 finally:
4078 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004079 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004080 if cpplint._cpplint_state.error_count != 0:
4081 return 1
4082 return 0
4083
4084
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004085def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004086 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004087 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004088 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004089 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004090 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004091 auth.add_auth_options(parser)
4092 options, args = parser.parse_args(args)
4093 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004094
sbc@chromium.org71437c02015-04-09 19:29:40 +00004095 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004096 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004097 return 1
4098
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004099 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004100 if args:
4101 base_branch = args[0]
4102 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004103 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004104 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004105
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004106 cl.RunHook(
4107 committing=not options.upload,
4108 may_prompt=False,
4109 verbose=options.verbose,
4110 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004111 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004112
4113
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004114def GenerateGerritChangeId(message):
4115 """Returns Ixxxxxx...xxx change id.
4116
4117 Works the same way as
4118 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4119 but can be called on demand on all platforms.
4120
4121 The basic idea is to generate git hash of a state of the tree, original commit
4122 message, author/committer info and timestamps.
4123 """
4124 lines = []
4125 tree_hash = RunGitSilent(['write-tree'])
4126 lines.append('tree %s' % tree_hash.strip())
4127 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4128 if code == 0:
4129 lines.append('parent %s' % parent.strip())
4130 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4131 lines.append('author %s' % author.strip())
4132 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4133 lines.append('committer %s' % committer.strip())
4134 lines.append('')
4135 # Note: Gerrit's commit-hook actually cleans message of some lines and
4136 # whitespace. This code is not doing this, but it clearly won't decrease
4137 # entropy.
4138 lines.append(message)
4139 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4140 stdin='\n'.join(lines))
4141 return 'I%s' % change_hash.strip()
4142
4143
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004144def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004145 """Computes the remote branch ref to use for the CL.
4146
4147 Args:
4148 remote (str): The git remote for the CL.
4149 remote_branch (str): The git remote branch for the CL.
4150 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004151 """
4152 if not (remote and remote_branch):
4153 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004154
wittman@chromium.org455dc922015-01-26 20:15:50 +00004155 if target_branch:
4156 # Cannonicalize branch references to the equivalent local full symbolic
4157 # refs, which are then translated into the remote full symbolic refs
4158 # below.
4159 if '/' not in target_branch:
4160 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4161 else:
4162 prefix_replacements = (
4163 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4164 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4165 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4166 )
4167 match = None
4168 for regex, replacement in prefix_replacements:
4169 match = re.search(regex, target_branch)
4170 if match:
4171 remote_branch = target_branch.replace(match.group(0), replacement)
4172 break
4173 if not match:
4174 # This is a branch path but not one we recognize; use as-is.
4175 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004176 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4177 # Handle the refs that need to land in different refs.
4178 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004179
wittman@chromium.org455dc922015-01-26 20:15:50 +00004180 # Create the true path to the remote branch.
4181 # Does the following translation:
4182 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4183 # * refs/remotes/origin/master -> refs/heads/master
4184 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4185 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4186 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4187 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4188 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4189 'refs/heads/')
4190 elif remote_branch.startswith('refs/remotes/branch-heads'):
4191 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004192
wittman@chromium.org455dc922015-01-26 20:15:50 +00004193 return remote_branch
4194
4195
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004196def cleanup_list(l):
4197 """Fixes a list so that comma separated items are put as individual items.
4198
4199 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4200 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4201 """
4202 items = sum((i.split(',') for i in l), [])
4203 stripped_items = (i.strip() for i in items)
4204 return sorted(filter(None, stripped_items))
4205
4206
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004207@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004208def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004209 """Uploads the current changelist to codereview.
4210
4211 Can skip dependency patchset uploads for a branch by running:
4212 git config branch.branch_name.skip-deps-uploads True
4213 To unset run:
4214 git config --unset branch.branch_name.skip-deps-uploads
4215 Can also set the above globally by using the --global flag.
4216 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004217 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4218 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004219 parser.add_option('--bypass-watchlists', action='store_true',
4220 dest='bypass_watchlists',
4221 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004222 parser.add_option('-f', action='store_true', dest='force',
4223 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004224 parser.add_option('--message', '-m', dest='message',
4225 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004226 parser.add_option('-b', '--bug',
4227 help='pre-populate the bug number(s) for this issue. '
4228 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004229 parser.add_option('--message-file', dest='message_file',
4230 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004231 parser.add_option('--title', '-t', dest='title',
4232 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004233 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004234 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004235 help='reviewer email addresses')
4236 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004237 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004238 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004239 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004240 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004241 parser.add_option('--emulate_svn_auto_props',
4242 '--emulate-svn-auto-props',
4243 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004244 dest="emulate_svn_auto_props",
4245 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004246 parser.add_option('-c', '--use-commit-queue', action='store_true',
4247 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004248 parser.add_option('--private', action='store_true',
4249 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004250 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004251 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004252 metavar='TARGET',
4253 help='Apply CL to remote ref TARGET. ' +
4254 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004255 parser.add_option('--squash', action='store_true',
4256 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004257 parser.add_option('--no-squash', action='store_true',
4258 help='Don\'t squash multiple commits into one ' +
4259 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004260 parser.add_option('--topic', default=None,
4261 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004262 parser.add_option('--email', default=None,
4263 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004264 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4265 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004266 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4267 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004268 help='Send the patchset to do a CQ dry run right after '
4269 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004270 parser.add_option('--dependencies', action='store_true',
4271 help='Uploads CLs of all the local branches that depend on '
4272 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004273
rmistry@google.com2dd99862015-06-22 12:22:18 +00004274 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004275 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004276 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004277 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004278 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004279 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004280 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004281
sbc@chromium.org71437c02015-04-09 19:29:40 +00004282 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004283 return 1
4284
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004285 options.reviewers = cleanup_list(options.reviewers)
4286 options.cc = cleanup_list(options.cc)
4287
tandriib80458a2016-06-23 12:20:07 -07004288 if options.message_file:
4289 if options.message:
4290 parser.error('only one of --message and --message-file allowed.')
4291 options.message = gclient_utils.FileRead(options.message_file)
4292 options.message_file = None
4293
tandrii4d0545a2016-07-06 03:56:49 -07004294 if options.cq_dry_run and options.use_commit_queue:
4295 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4296
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004297 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4298 settings.GetIsGerrit()
4299
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004300 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004301 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004302
4303
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004304@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004305def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004306 """DEPRECATED: Used to commit the current changelist via git-svn."""
4307 message = ('git-cl no longer supports committing to SVN repositories via '
4308 'git-svn. You probably want to use `git cl land` instead.')
4309 print(message)
4310 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004311
4312
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004313@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004314def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004315 """Commits the current changelist via git.
4316
4317 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4318 upstream and closes the issue automatically and atomically.
4319
4320 Otherwise (in case of Rietveld):
4321 Squashes branch into a single commit.
4322 Updates commit message with metadata (e.g. pointer to review).
4323 Pushes the code upstream.
4324 Updates review and closes.
4325 """
4326 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4327 help='bypass upload presubmit hook')
4328 parser.add_option('-m', dest='message',
4329 help="override review description")
4330 parser.add_option('-f', action='store_true', dest='force',
4331 help="force yes to questions (don't prompt)")
4332 parser.add_option('-c', dest='contributor',
4333 help="external contributor for patch (appended to " +
4334 "description and used as author for git). Should be " +
4335 "formatted as 'First Last <email@example.com>'")
4336 add_git_similarity(parser)
4337 auth.add_auth_options(parser)
4338 (options, args) = parser.parse_args(args)
4339 auth_config = auth.extract_auth_config_from_options(options)
4340
4341 cl = Changelist(auth_config=auth_config)
4342
4343 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4344 if cl.IsGerrit():
4345 if options.message:
4346 # This could be implemented, but it requires sending a new patch to
4347 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4348 # Besides, Gerrit has the ability to change the commit message on submit
4349 # automatically, thus there is no need to support this option (so far?).
4350 parser.error('-m MESSAGE option is not supported for Gerrit.')
4351 if options.contributor:
4352 parser.error(
4353 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4354 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4355 'the contributor\'s "name <email>". If you can\'t upload such a '
4356 'commit for review, contact your repository admin and request'
4357 '"Forge-Author" permission.')
4358 if not cl.GetIssue():
4359 DieWithError('You must upload the change first to Gerrit.\n'
4360 ' If you would rather have `git cl land` upload '
4361 'automatically for you, see http://crbug.com/642759')
4362 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4363 options.verbose)
4364
4365 current = cl.GetBranch()
4366 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4367 if remote == '.':
4368 print()
4369 print('Attempting to push branch %r into another local branch!' % current)
4370 print()
4371 print('Either reparent this branch on top of origin/master:')
4372 print(' git reparent-branch --root')
4373 print()
4374 print('OR run `git rebase-update` if you think the parent branch is ')
4375 print('already committed.')
4376 print()
4377 print(' Current parent: %r' % upstream_branch)
4378 return 1
4379
4380 if not args:
4381 # Default to merging against our best guess of the upstream branch.
4382 args = [cl.GetUpstreamBranch()]
4383
4384 if options.contributor:
4385 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4386 print("Please provide contibutor as 'First Last <email@example.com>'")
4387 return 1
4388
4389 base_branch = args[0]
4390
4391 if git_common.is_dirty_git_tree('land'):
4392 return 1
4393
4394 # This rev-list syntax means "show all commits not in my branch that
4395 # are in base_branch".
4396 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4397 base_branch]).splitlines()
4398 if upstream_commits:
4399 print('Base branch "%s" has %d commits '
4400 'not in this branch.' % (base_branch, len(upstream_commits)))
4401 print('Run "git merge %s" before attempting to land.' % base_branch)
4402 return 1
4403
4404 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4405 if not options.bypass_hooks:
4406 author = None
4407 if options.contributor:
4408 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4409 hook_results = cl.RunHook(
4410 committing=True,
4411 may_prompt=not options.force,
4412 verbose=options.verbose,
4413 change=cl.GetChange(merge_base, author))
4414 if not hook_results.should_continue():
4415 return 1
4416
4417 # Check the tree status if the tree status URL is set.
4418 status = GetTreeStatus()
4419 if 'closed' == status:
4420 print('The tree is closed. Please wait for it to reopen. Use '
4421 '"git cl land --bypass-hooks" to commit on a closed tree.')
4422 return 1
4423 elif 'unknown' == status:
4424 print('Unable to determine tree status. Please verify manually and '
4425 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4426 return 1
4427
4428 change_desc = ChangeDescription(options.message)
4429 if not change_desc.description and cl.GetIssue():
4430 change_desc = ChangeDescription(cl.GetDescription())
4431
4432 if not change_desc.description:
4433 if not cl.GetIssue() and options.bypass_hooks:
4434 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4435 else:
4436 print('No description set.')
4437 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4438 return 1
4439
4440 # Keep a separate copy for the commit message, because the commit message
4441 # contains the link to the Rietveld issue, while the Rietveld message contains
4442 # the commit viewvc url.
4443 if cl.GetIssue():
4444 change_desc.update_reviewers(cl.GetApprovingReviewers())
4445
4446 commit_desc = ChangeDescription(change_desc.description)
4447 if cl.GetIssue():
4448 # Xcode won't linkify this URL unless there is a non-whitespace character
4449 # after it. Add a period on a new line to circumvent this. Also add a space
4450 # before the period to make sure that Gitiles continues to correctly resolve
4451 # the URL.
4452 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4453 if options.contributor:
4454 commit_desc.append_footer('Patch from %s.' % options.contributor)
4455
4456 print('Description:')
4457 print(commit_desc.description)
4458
4459 branches = [merge_base, cl.GetBranchRef()]
4460 if not options.force:
4461 print_stats(options.similarity, options.find_copies, branches)
4462
4463 # We want to squash all this branch's commits into one commit with the proper
4464 # description. We do this by doing a "reset --soft" to the base branch (which
4465 # keeps the working copy the same), then landing that.
4466 MERGE_BRANCH = 'git-cl-commit'
4467 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4468 # Delete the branches if they exist.
4469 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4470 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4471 result = RunGitWithCode(showref_cmd)
4472 if result[0] == 0:
4473 RunGit(['branch', '-D', branch])
4474
4475 # We might be in a directory that's present in this branch but not in the
4476 # trunk. Move up to the top of the tree so that git commands that expect a
4477 # valid CWD won't fail after we check out the merge branch.
4478 rel_base_path = settings.GetRelativeRoot()
4479 if rel_base_path:
4480 os.chdir(rel_base_path)
4481
4482 # Stuff our change into the merge branch.
4483 # We wrap in a try...finally block so if anything goes wrong,
4484 # we clean up the branches.
4485 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004486 revision = None
4487 try:
4488 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4489 RunGit(['reset', '--soft', merge_base])
4490 if options.contributor:
4491 RunGit(
4492 [
4493 'commit', '--author', options.contributor,
4494 '-m', commit_desc.description,
4495 ])
4496 else:
4497 RunGit(['commit', '-m', commit_desc.description])
4498
4499 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4500 mirror = settings.GetGitMirror(remote)
4501 if mirror:
4502 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004503 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004504 else:
4505 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004506 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004507 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4508
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004509 if git_numberer_enabled:
4510 # TODO(tandrii): maybe do autorebase + retry on failure
4511 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004512 logging.debug('Adding git number footers')
4513 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4514 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4515 branch)
4516 # Ensure timestamps are monotonically increasing.
4517 timestamp = max(1 + _get_committer_timestamp(merge_base),
4518 _get_committer_timestamp('HEAD'))
4519 _git_amend_head(commit_desc.description, timestamp)
4520 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004521
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004522 retcode, output = RunGitWithCode(
4523 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004524 if retcode == 0:
4525 revision = RunGit(['rev-parse', 'HEAD']).strip()
4526 logging.debug(output)
4527 except: # pylint: disable=bare-except
4528 if _IS_BEING_TESTED:
4529 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4530 + '-' * 30 + '8<' + '-' * 30)
4531 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4532 raise
4533 finally:
4534 # And then swap back to the original branch and clean up.
4535 RunGit(['checkout', '-q', cl.GetBranch()])
4536 RunGit(['branch', '-D', MERGE_BRANCH])
4537
4538 if not revision:
4539 print('Failed to push. If this persists, please file a bug.')
4540 return 1
4541
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004542 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004543 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004544 if viewvc_url and revision:
4545 change_desc.append_footer(
4546 'Committed: %s%s' % (viewvc_url, revision))
4547 elif revision:
4548 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004549 print('Closing issue '
4550 '(you may be prompted for your codereview password)...')
4551 cl.UpdateDescription(change_desc.description)
4552 cl.CloseIssue()
4553 props = cl.GetIssueProperties()
4554 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004555 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4556 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004557 if options.bypass_hooks:
4558 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4559 else:
4560 comment += ' (presubmit successful).'
4561 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4562
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004563 if os.path.isfile(POSTUPSTREAM_HOOK):
4564 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4565
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004566 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004567
4568
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004569@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004570def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004571 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004572 parser.add_option('-b', dest='newbranch',
4573 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004574 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004575 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004576 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4577 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004578 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004579 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004580 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004581 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004582 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004583 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004584
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004585
4586 group = optparse.OptionGroup(
4587 parser,
4588 'Options for continuing work on the current issue uploaded from a '
4589 'different clone (e.g. different machine). Must be used independently '
4590 'from the other options. No issue number should be specified, and the '
4591 'branch must have an issue number associated with it')
4592 group.add_option('--reapply', action='store_true', dest='reapply',
4593 help='Reset the branch and reapply the issue.\n'
4594 'CAUTION: This will undo any local changes in this '
4595 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004596
4597 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004598 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004599 parser.add_option_group(group)
4600
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004601 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004602 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004603 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004604 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004605 auth_config = auth.extract_auth_config_from_options(options)
4606
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004607
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004608 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004609 if options.newbranch:
4610 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004611 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004612 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004613
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004614 cl = Changelist(auth_config=auth_config,
4615 codereview=options.forced_codereview)
4616 if not cl.GetIssue():
4617 parser.error('current branch must have an associated issue')
4618
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004619 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004620 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004621 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004622
4623 RunGit(['reset', '--hard', upstream])
4624 if options.pull:
4625 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004626
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004627 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4628 options.directory)
4629
4630 if len(args) != 1 or not args[0]:
4631 parser.error('Must specify issue number or url')
4632
4633 # We don't want uncommitted changes mixed up with the patch.
4634 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004635 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004636
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004637 if options.newbranch:
4638 if options.force:
4639 RunGit(['branch', '-D', options.newbranch],
4640 stderr=subprocess2.PIPE, error_ok=True)
4641 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004642 elif not GetCurrentBranch():
4643 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004644
4645 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4646
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004647 if cl.IsGerrit():
4648 if options.reject:
4649 parser.error('--reject is not supported with Gerrit codereview.')
4650 if options.nocommit:
4651 parser.error('--nocommit is not supported with Gerrit codereview.')
4652 if options.directory:
4653 parser.error('--directory is not supported with Gerrit codereview.')
4654
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004655 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004656 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004657
4658
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004659def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004660 """Fetches the tree status and returns either 'open', 'closed',
4661 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004662 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004663 if url:
4664 status = urllib2.urlopen(url).read().lower()
4665 if status.find('closed') != -1 or status == '0':
4666 return 'closed'
4667 elif status.find('open') != -1 or status == '1':
4668 return 'open'
4669 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004670 return 'unset'
4671
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004672
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004673def GetTreeStatusReason():
4674 """Fetches the tree status from a json url and returns the message
4675 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004676 url = settings.GetTreeStatusUrl()
4677 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004678 connection = urllib2.urlopen(json_url)
4679 status = json.loads(connection.read())
4680 connection.close()
4681 return status['message']
4682
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004683
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004684def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004685 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004686 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004687 status = GetTreeStatus()
4688 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004689 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004690 return 2
4691
vapiera7fbd5a2016-06-16 09:17:49 -07004692 print('The tree is %s' % status)
4693 print()
4694 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004695 if status != 'open':
4696 return 1
4697 return 0
4698
4699
maruel@chromium.org15192402012-09-06 12:38:29 +00004700def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004701 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004702 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004703 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004704 '-b', '--bot', action='append',
4705 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4706 'times to specify multiple builders. ex: '
4707 '"-b win_rel -b win_layout". See '
4708 'the try server waterfall for the builders name and the tests '
4709 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004710 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004711 '-B', '--bucket', default='',
4712 help=('Buildbucket bucket to send the try requests.'))
4713 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004714 '-m', '--master', default='',
4715 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004716 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004717 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004718 help='Revision to use for the try job; default: the revision will '
4719 'be determined by the try recipe that builder runs, which usually '
4720 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004721 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004722 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004723 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004724 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004725 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004726 '--project',
4727 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004728 'in recipe to determine to which repository or directory to '
4729 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004730 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004731 '-p', '--property', dest='properties', action='append', default=[],
4732 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004733 'key2=value2 etc. The value will be treated as '
4734 'json if decodable, or as string otherwise. '
4735 'NOTE: using this may make your try job not usable for CQ, '
4736 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004737 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004738 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4739 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004740 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004741 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004742 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004743 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004744
machenbach@chromium.org45453142015-09-15 08:45:22 +00004745 # Make sure that all properties are prop=value pairs.
4746 bad_params = [x for x in options.properties if '=' not in x]
4747 if bad_params:
4748 parser.error('Got properties with missing "=": %s' % bad_params)
4749
maruel@chromium.org15192402012-09-06 12:38:29 +00004750 if args:
4751 parser.error('Unknown arguments: %s' % args)
4752
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004753 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004754 if not cl.GetIssue():
4755 parser.error('Need to upload first')
4756
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004757 if cl.IsGerrit():
4758 # HACK: warm up Gerrit change detail cache to save on RPCs.
4759 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4760
tandriie113dfd2016-10-11 10:20:12 -07004761 error_message = cl.CannotTriggerTryJobReason()
4762 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004763 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004764
borenet6c0efe62016-10-19 08:13:29 -07004765 if options.bucket and options.master:
4766 parser.error('Only one of --bucket and --master may be used.')
4767
qyearsley1fdfcb62016-10-24 13:22:03 -07004768 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004769
qyearsleydd49f942016-10-28 11:57:22 -07004770 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4771 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004772 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004773 if options.verbose:
4774 print('git cl try with no bots now defaults to CQ Dry Run.')
4775 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004776
borenet6c0efe62016-10-19 08:13:29 -07004777 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004778 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004779 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004780 'of bot requires an initial job from a parent (usually a builder). '
4781 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004782 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004783 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004784
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004785 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004786 # TODO(tandrii): Checking local patchset against remote patchset is only
4787 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4788 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004789 print('Warning: Codereview server has newer patchsets (%s) than most '
4790 'recent upload from local checkout (%s). Did a previous upload '
4791 'fail?\n'
4792 'By default, git cl try uses the latest patchset from '
4793 'codereview, continuing to use patchset %s.\n' %
4794 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004795
tandrii568043b2016-10-11 07:49:18 -07004796 try:
borenet6c0efe62016-10-19 08:13:29 -07004797 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4798 patchset)
tandrii568043b2016-10-11 07:49:18 -07004799 except BuildbucketResponseException as ex:
4800 print('ERROR: %s' % ex)
4801 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004802 return 0
4803
4804
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004805def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004806 """Prints info about try jobs associated with current CL."""
4807 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004808 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004809 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004810 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004811 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004812 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004813 '--color', action='store_true', default=setup_color.IS_TTY,
4814 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004815 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004816 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4817 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004818 group.add_option(
4819 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004820 parser.add_option_group(group)
4821 auth.add_auth_options(parser)
4822 options, args = parser.parse_args(args)
4823 if args:
4824 parser.error('Unrecognized args: %s' % ' '.join(args))
4825
4826 auth_config = auth.extract_auth_config_from_options(options)
4827 cl = Changelist(auth_config=auth_config)
4828 if not cl.GetIssue():
4829 parser.error('Need to upload first')
4830
tandrii221ab252016-10-06 08:12:04 -07004831 patchset = options.patchset
4832 if not patchset:
4833 patchset = cl.GetMostRecentPatchset()
4834 if not patchset:
4835 parser.error('Codereview doesn\'t know about issue %s. '
4836 'No access to issue or wrong issue number?\n'
4837 'Either upload first, or pass --patchset explicitely' %
4838 cl.GetIssue())
4839
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004840 # TODO(tandrii): Checking local patchset against remote patchset is only
4841 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4842 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004843 print('Warning: Codereview server has newer patchsets (%s) than most '
4844 'recent upload from local checkout (%s). Did a previous upload '
4845 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004846 'By default, git cl try-results uses the latest patchset from '
4847 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004848 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004849 try:
tandrii221ab252016-10-06 08:12:04 -07004850 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004851 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004852 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004853 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004854 if options.json:
4855 write_try_results_json(options.json, jobs)
4856 else:
4857 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004858 return 0
4859
4860
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004861@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004862def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004863 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004864 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004865 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004866 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004867
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004868 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004869 if args:
4870 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004871 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004872 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004873 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004874 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004875
4876 # Clear configured merge-base, if there is one.
4877 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004878 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004879 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004880 return 0
4881
4882
thestig@chromium.org00858c82013-12-02 23:08:03 +00004883def CMDweb(parser, args):
4884 """Opens the current CL in the web browser."""
4885 _, args = parser.parse_args(args)
4886 if args:
4887 parser.error('Unrecognized args: %s' % ' '.join(args))
4888
4889 issue_url = Changelist().GetIssueURL()
4890 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004891 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004892 return 1
4893
4894 webbrowser.open(issue_url)
4895 return 0
4896
4897
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004898def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004899 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004900 parser.add_option('-d', '--dry-run', action='store_true',
4901 help='trigger in dry run mode')
4902 parser.add_option('-c', '--clear', action='store_true',
4903 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004904 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004905 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004906 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004907 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004908 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004909 if args:
4910 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004911 if options.dry_run and options.clear:
4912 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4913
iannuccie53c9352016-08-17 14:40:40 -07004914 cl = Changelist(auth_config=auth_config, issue=options.issue,
4915 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004916 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004917 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004918 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004919 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004920 state = _CQState.DRY_RUN
4921 else:
4922 state = _CQState.COMMIT
4923 if not cl.GetIssue():
4924 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004925 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004926 return 0
4927
4928
groby@chromium.org411034a2013-02-26 15:12:01 +00004929def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004930 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004931 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004932 auth.add_auth_options(parser)
4933 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004934 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004935 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004936 if args:
4937 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004938 cl = Changelist(auth_config=auth_config, issue=options.issue,
4939 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004940 # Ensure there actually is an issue to close.
4941 cl.GetDescription()
4942 cl.CloseIssue()
4943 return 0
4944
4945
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004946def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004947 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004948 parser.add_option(
4949 '--stat',
4950 action='store_true',
4951 dest='stat',
4952 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004953 auth.add_auth_options(parser)
4954 options, args = parser.parse_args(args)
4955 auth_config = auth.extract_auth_config_from_options(options)
4956 if args:
4957 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004958
4959 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004960 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004961 # Staged changes would be committed along with the patch from last
4962 # upload, hence counted toward the "last upload" side in the final
4963 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004964 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004965 return 1
4966
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004967 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004968 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004969 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004970 if not issue:
4971 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004972 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004973 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004974
4975 # Create a new branch based on the merge-base
4976 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004977 # Clear cached branch in cl object, to avoid overwriting original CL branch
4978 # properties.
4979 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004980 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004981 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004982 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004983 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004984 return rtn
4985
wychen@chromium.org06928532015-02-03 02:11:29 +00004986 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004987 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004988 cmd = ['git', 'diff']
4989 if options.stat:
4990 cmd.append('--stat')
4991 cmd.extend([TMP_BRANCH, branch, '--'])
4992 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004993 finally:
4994 RunGit(['checkout', '-q', branch])
4995 RunGit(['branch', '-D', TMP_BRANCH])
4996
4997 return 0
4998
4999
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005000def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005001 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005002 parser.add_option(
5003 '--no-color',
5004 action='store_true',
5005 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005006 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005007 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005008 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005009
5010 author = RunGit(['config', 'user.email']).strip() or None
5011
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005012 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005013
5014 if args:
5015 if len(args) > 1:
5016 parser.error('Unknown args')
5017 base_branch = args[0]
5018 else:
5019 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005020 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005021
5022 change = cl.GetChange(base_branch, None)
5023 return owners_finder.OwnersFinder(
5024 [f.LocalPath() for f in
5025 cl.GetChange(base_branch, None).AffectedFiles()],
5026 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005027 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005028 disable_color=options.no_color).run()
5029
5030
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005031def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005032 """Generates a diff command."""
5033 # Generate diff for the current branch's changes.
5034 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005035 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005036
5037 if args:
5038 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005039 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005040 diff_cmd.append(arg)
5041 else:
5042 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005043
5044 return diff_cmd
5045
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005046
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005047def MatchingFileType(file_name, extensions):
5048 """Returns true if the file name ends with one of the given extensions."""
5049 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005050
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005051
enne@chromium.org555cfe42014-01-29 18:21:39 +00005052@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005053def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005054 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005055 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005056 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005057 parser.add_option('--full', action='store_true',
5058 help='Reformat the full content of all touched files')
5059 parser.add_option('--dry-run', action='store_true',
5060 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005061 parser.add_option('--python', action='store_true',
5062 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005063 parser.add_option('--js', action='store_true',
5064 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005065 parser.add_option('--diff', action='store_true',
5066 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005067 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005068
Daniel Chengc55eecf2016-12-30 03:11:02 -08005069 # Normalize any remaining args against the current path, so paths relative to
5070 # the current directory are still resolved as expected.
5071 args = [os.path.join(os.getcwd(), arg) for arg in args]
5072
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005073 # git diff generates paths against the root of the repository. Change
5074 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005075 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005076 if rel_base_path:
5077 os.chdir(rel_base_path)
5078
digit@chromium.org29e47272013-05-17 17:01:46 +00005079 # Grab the merge-base commit, i.e. the upstream commit of the current
5080 # branch when it was created or the last time it was rebased. This is
5081 # to cover the case where the user may have called "git fetch origin",
5082 # moving the origin branch to a newer commit, but hasn't rebased yet.
5083 upstream_commit = None
5084 cl = Changelist()
5085 upstream_branch = cl.GetUpstreamBranch()
5086 if upstream_branch:
5087 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5088 upstream_commit = upstream_commit.strip()
5089
5090 if not upstream_commit:
5091 DieWithError('Could not find base commit for this branch. '
5092 'Are you in detached state?')
5093
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005094 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5095 diff_output = RunGit(changed_files_cmd)
5096 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005097 # Filter out files deleted by this CL
5098 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005099
Christopher Lamc5ba6922017-01-24 11:19:14 +11005100 if opts.js:
5101 CLANG_EXTS.append('.js')
5102
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005103 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5104 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5105 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005106 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005107
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005108 top_dir = os.path.normpath(
5109 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5110
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005111 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5112 # formatted. This is used to block during the presubmit.
5113 return_value = 0
5114
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005115 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005116 # Locate the clang-format binary in the checkout
5117 try:
5118 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005119 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005120 DieWithError(e)
5121
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005122 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005123 cmd = [clang_format_tool]
5124 if not opts.dry_run and not opts.diff:
5125 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005126 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005127 if opts.diff:
5128 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005129 else:
5130 env = os.environ.copy()
5131 env['PATH'] = str(os.path.dirname(clang_format_tool))
5132 try:
5133 script = clang_format.FindClangFormatScriptInChromiumTree(
5134 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005135 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005136 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005137
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005138 cmd = [sys.executable, script, '-p0']
5139 if not opts.dry_run and not opts.diff:
5140 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005141
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005142 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5143 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005144
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005145 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5146 if opts.diff:
5147 sys.stdout.write(stdout)
5148 if opts.dry_run and len(stdout) > 0:
5149 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005150
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005151 # Similar code to above, but using yapf on .py files rather than clang-format
5152 # on C/C++ files
5153 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005154 yapf_tool = gclient_utils.FindExecutable('yapf')
5155 if yapf_tool is None:
5156 DieWithError('yapf not found in PATH')
5157
5158 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005159 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005160 cmd = [yapf_tool]
5161 if not opts.dry_run and not opts.diff:
5162 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005163 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005164 if opts.diff:
5165 sys.stdout.write(stdout)
5166 else:
5167 # TODO(sbc): yapf --lines mode still has some issues.
5168 # https://github.com/google/yapf/issues/154
5169 DieWithError('--python currently only works with --full')
5170
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005171 # Dart's formatter does not have the nice property of only operating on
5172 # modified chunks, so hard code full.
5173 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005174 try:
5175 command = [dart_format.FindDartFmtToolInChromiumTree()]
5176 if not opts.dry_run and not opts.diff:
5177 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005178 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005179
ppi@chromium.org6593d932016-03-03 15:41:15 +00005180 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005181 if opts.dry_run and stdout:
5182 return_value = 2
5183 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005184 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5185 'found in this checkout. Files in other languages are still '
5186 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005187
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005188 # Format GN build files. Always run on full build files for canonical form.
5189 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005190 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005191 if opts.dry_run or opts.diff:
5192 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005193 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005194 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5195 shell=sys.platform == 'win32',
5196 cwd=top_dir)
5197 if opts.dry_run and gn_ret == 2:
5198 return_value = 2 # Not formatted.
5199 elif opts.diff and gn_ret == 2:
5200 # TODO this should compute and print the actual diff.
5201 print("This change has GN build file diff for " + gn_diff_file)
5202 elif gn_ret != 0:
5203 # For non-dry run cases (and non-2 return values for dry-run), a
5204 # nonzero error code indicates a failure, probably because the file
5205 # doesn't parse.
5206 DieWithError("gn format failed on " + gn_diff_file +
5207 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005208
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005209 metrics_xml_files = [
5210 'tools/metrics/actions/actions.xml',
5211 'tools/metrics/histograms/histograms.xml',
5212 'tools/metrics/rappor/rappor.xml']
5213 for xml_file in metrics_xml_files:
5214 if xml_file in diff_files:
5215 tool_dir = top_dir + '/' + os.path.dirname(xml_file)
5216 cmd = [tool_dir + '/pretty_print.py', '--non-interactive']
5217 if opts.dry_run or opts.diff:
5218 cmd.append('--diff')
5219 stdout = RunCommand(cmd, cwd=top_dir)
5220 if opts.diff:
5221 sys.stdout.write(stdout)
5222 if opts.dry_run and stdout:
5223 return_value = 2 # Not formatted.
5224
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005225 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005226
5227
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005228@subcommand.usage('<codereview url or issue id>')
5229def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005230 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005231 _, args = parser.parse_args(args)
5232
5233 if len(args) != 1:
5234 parser.print_help()
5235 return 1
5236
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005237 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005238 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005239 parser.print_help()
5240 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005241 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005242
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005243 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005244 output = RunGit(['config', '--local', '--get-regexp',
5245 r'branch\..*\.%s' % issueprefix],
5246 error_ok=True)
5247 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005248 if issue == target_issue:
5249 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005250
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005251 branches = []
5252 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005253 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005254 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005255 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005256 return 1
5257 if len(branches) == 1:
5258 RunGit(['checkout', branches[0]])
5259 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005260 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005261 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005262 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005263 which = raw_input('Choose by index: ')
5264 try:
5265 RunGit(['checkout', branches[int(which)]])
5266 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005267 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005268 return 1
5269
5270 return 0
5271
5272
maruel@chromium.org29404b52014-09-08 22:58:00 +00005273def CMDlol(parser, args):
5274 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005275 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005276 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5277 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5278 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005279 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005280 return 0
5281
5282
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005283class OptionParser(optparse.OptionParser):
5284 """Creates the option parse and add --verbose support."""
5285 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005286 optparse.OptionParser.__init__(
5287 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005288 self.add_option(
5289 '-v', '--verbose', action='count', default=0,
5290 help='Use 2 times for more debugging info')
5291
5292 def parse_args(self, args=None, values=None):
5293 options, args = optparse.OptionParser.parse_args(self, args, values)
5294 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005295 logging.basicConfig(
5296 level=levels[min(options.verbose, len(levels) - 1)],
5297 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5298 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005299 return options, args
5300
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005301
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005302def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005303 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005304 print('\nYour python version %s is unsupported, please upgrade.\n' %
5305 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005306 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005307
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005308 # Reload settings.
5309 global settings
5310 settings = Settings()
5311
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005312 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005313 dispatcher = subcommand.CommandDispatcher(__name__)
5314 try:
5315 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005316 except auth.AuthenticationError as e:
5317 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005318 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005319 if e.code != 500:
5320 raise
5321 DieWithError(
5322 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5323 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005324 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005325
5326
5327if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005328 # These affect sys.stdout so do it outside of main() to simplify mocks in
5329 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005330 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005331 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005332 try:
5333 sys.exit(main(sys.argv[1:]))
5334 except KeyboardInterrupt:
5335 sys.stderr.write('interrupted\n')
5336 sys.exit(1)