blob: 320095282b23aa21b101bb6fb5bb4faa15c72cf6 [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():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001361 self.description = self._codereview_impl.FetchDescription()
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
1506 # Make sure authenticated to codereview before running potentially expensive
1507 # hooks. It is a fast, best efforts check. Codereview still can reject the
1508 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001509 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001510
1511 # Apply watchlists on upload.
1512 change = self.GetChange(base_branch, None)
1513 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1514 files = [f.LocalPath() for f in change.AffectedFiles()]
1515 if not options.bypass_watchlists:
1516 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1517
1518 if not options.bypass_hooks:
1519 if options.reviewers or options.tbr_owners:
1520 # Set the reviewer list now so that presubmit checks can access it.
1521 change_description = ChangeDescription(change.FullDescriptionText())
1522 change_description.update_reviewers(options.reviewers,
1523 options.tbr_owners,
1524 change)
1525 change.SetDescriptionText(change_description.description)
1526 hook_results = self.RunHook(committing=False,
1527 may_prompt=not options.force,
1528 verbose=options.verbose,
1529 change=change)
1530 if not hook_results.should_continue():
1531 return 1
1532 if not options.reviewers and hook_results.reviewers:
1533 options.reviewers = hook_results.reviewers.split(',')
1534
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001535 # TODO(tandrii): Checking local patchset against remote patchset is only
1536 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1537 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001538 latest_patchset = self.GetMostRecentPatchset()
1539 local_patchset = self.GetPatchset()
1540 if (latest_patchset and local_patchset and
1541 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001542 print('The last upload made from this repository was patchset #%d but '
1543 'the most recent patchset on the server is #%d.'
1544 % (local_patchset, latest_patchset))
1545 print('Uploading will still work, but if you\'ve uploaded to this '
1546 'issue from another machine or branch the patch you\'re '
1547 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 ask_for_data('About to upload; enter to confirm.')
1549
1550 print_stats(options.similarity, options.find_copies, git_diff_args)
1551 ret = self.CMDUploadChange(options, git_diff_args, change)
1552 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001553 if options.use_commit_queue:
1554 self.SetCQState(_CQState.COMMIT)
1555 elif options.cq_dry_run:
1556 self.SetCQState(_CQState.DRY_RUN)
1557
tandrii5d48c322016-08-18 16:19:37 -07001558 _git_set_branch_config_value('last-upload-hash',
1559 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001560 # Run post upload hooks, if specified.
1561 if settings.GetRunPostUploadHook():
1562 presubmit_support.DoPostUploadExecuter(
1563 change,
1564 self,
1565 settings.GetRoot(),
1566 options.verbose,
1567 sys.stdout)
1568
1569 # Upload all dependencies if specified.
1570 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001571 print()
1572 print('--dependencies has been specified.')
1573 print('All dependent local branches will be re-uploaded.')
1574 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001575 # Remove the dependencies flag from args so that we do not end up in a
1576 # loop.
1577 orig_args.remove('--dependencies')
1578 ret = upload_branch_deps(self, orig_args)
1579 return ret
1580
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001581 def SetCQState(self, new_state):
1582 """Update the CQ state for latest patchset.
1583
1584 Issue must have been already uploaded and known.
1585 """
1586 assert new_state in _CQState.ALL_STATES
1587 assert self.GetIssue()
1588 return self._codereview_impl.SetCQState(new_state)
1589
qyearsley1fdfcb62016-10-24 13:22:03 -07001590 def TriggerDryRun(self):
1591 """Triggers a dry run and prints a warning on failure."""
1592 # TODO(qyearsley): Either re-use this method in CMDset_commit
1593 # and CMDupload, or change CMDtry to trigger dry runs with
1594 # just SetCQState, and catch keyboard interrupt and other
1595 # errors in that method.
1596 try:
1597 self.SetCQState(_CQState.DRY_RUN)
1598 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1599 return 0
1600 except KeyboardInterrupt:
1601 raise
1602 except:
1603 print('WARNING: failed to trigger CQ Dry Run.\n'
1604 'Either:\n'
1605 ' * your project has no CQ\n'
1606 ' * you don\'t have permission to trigger Dry Run\n'
1607 ' * bug in this code (see stack trace below).\n'
1608 'Consider specifying which bots to trigger manually '
1609 'or asking your project owners for permissions '
1610 'or contacting Chrome Infrastructure team at '
1611 'https://www.chromium.org/infra\n\n')
1612 # Still raise exception so that stack trace is printed.
1613 raise
1614
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001615 # Forward methods to codereview specific implementation.
1616
1617 def CloseIssue(self):
1618 return self._codereview_impl.CloseIssue()
1619
1620 def GetStatus(self):
1621 return self._codereview_impl.GetStatus()
1622
1623 def GetCodereviewServer(self):
1624 return self._codereview_impl.GetCodereviewServer()
1625
tandriide281ae2016-10-12 06:02:30 -07001626 def GetIssueOwner(self):
1627 """Get owner from codereview, which may differ from this checkout."""
1628 return self._codereview_impl.GetIssueOwner()
1629
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001630 def GetApprovingReviewers(self):
1631 return self._codereview_impl.GetApprovingReviewers()
1632
1633 def GetMostRecentPatchset(self):
1634 return self._codereview_impl.GetMostRecentPatchset()
1635
tandriide281ae2016-10-12 06:02:30 -07001636 def CannotTriggerTryJobReason(self):
1637 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1638 return self._codereview_impl.CannotTriggerTryJobReason()
1639
tandrii8c5a3532016-11-04 07:52:02 -07001640 def GetTryjobProperties(self, patchset=None):
1641 """Returns dictionary of properties to launch tryjob."""
1642 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1643
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001644 def __getattr__(self, attr):
1645 # This is because lots of untested code accesses Rietveld-specific stuff
1646 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001647 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001648 # Note that child method defines __getattr__ as well, and forwards it here,
1649 # because _RietveldChangelistImpl is not cleaned up yet, and given
1650 # deprecation of Rietveld, it should probably be just removed.
1651 # Until that time, avoid infinite recursion by bypassing __getattr__
1652 # of implementation class.
1653 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001654
1655
1656class _ChangelistCodereviewBase(object):
1657 """Abstract base class encapsulating codereview specifics of a changelist."""
1658 def __init__(self, changelist):
1659 self._changelist = changelist # instance of Changelist
1660
1661 def __getattr__(self, attr):
1662 # Forward methods to changelist.
1663 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1664 # _RietveldChangelistImpl to avoid this hack?
1665 return getattr(self._changelist, attr)
1666
1667 def GetStatus(self):
1668 """Apply a rough heuristic to give a simple summary of an issue's review
1669 or CQ status, assuming adherence to a common workflow.
1670
1671 Returns None if no issue for this branch, or specific string keywords.
1672 """
1673 raise NotImplementedError()
1674
1675 def GetCodereviewServer(self):
1676 """Returns server URL without end slash, like "https://codereview.com"."""
1677 raise NotImplementedError()
1678
1679 def FetchDescription(self):
1680 """Fetches and returns description from the codereview server."""
1681 raise NotImplementedError()
1682
tandrii5d48c322016-08-18 16:19:37 -07001683 @classmethod
1684 def IssueConfigKey(cls):
1685 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001686 raise NotImplementedError()
1687
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001688 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001689 def PatchsetConfigKey(cls):
1690 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001691 raise NotImplementedError()
1692
tandrii5d48c322016-08-18 16:19:37 -07001693 @classmethod
1694 def CodereviewServerConfigKey(cls):
1695 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001696 raise NotImplementedError()
1697
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001698 def _PostUnsetIssueProperties(self):
1699 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001700 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001701
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001702 def GetRieveldObjForPresubmit(self):
1703 # This is an unfortunate Rietveld-embeddedness in presubmit.
1704 # For non-Rietveld codereviews, this probably should return a dummy object.
1705 raise NotImplementedError()
1706
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001707 def GetGerritObjForPresubmit(self):
1708 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1709 return None
1710
dsansomee2d6fd92016-09-08 00:10:47 -07001711 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001712 """Update the description on codereview site."""
1713 raise NotImplementedError()
1714
1715 def CloseIssue(self):
1716 """Closes the issue."""
1717 raise NotImplementedError()
1718
1719 def GetApprovingReviewers(self):
1720 """Returns a list of reviewers approving the change.
1721
1722 Note: not necessarily committers.
1723 """
1724 raise NotImplementedError()
1725
1726 def GetMostRecentPatchset(self):
1727 """Returns the most recent patchset number from the codereview site."""
1728 raise NotImplementedError()
1729
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001730 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1731 directory):
1732 """Fetches and applies the issue.
1733
1734 Arguments:
1735 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1736 reject: if True, reject the failed patch instead of switching to 3-way
1737 merge. Rietveld only.
1738 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1739 only.
1740 directory: switch to directory before applying the patch. Rietveld only.
1741 """
1742 raise NotImplementedError()
1743
1744 @staticmethod
1745 def ParseIssueURL(parsed_url):
1746 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1747 failed."""
1748 raise NotImplementedError()
1749
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001750 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001751 """Best effort check that user is authenticated with codereview server.
1752
1753 Arguments:
1754 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001755 refresh: whether to attempt to refresh credentials. Ignored if not
1756 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001757 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001758 raise NotImplementedError()
1759
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001760 def CMDUploadChange(self, options, args, change):
1761 """Uploads a change to codereview."""
1762 raise NotImplementedError()
1763
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001764 def SetCQState(self, new_state):
1765 """Update the CQ state for latest patchset.
1766
1767 Issue must have been already uploaded and known.
1768 """
1769 raise NotImplementedError()
1770
tandriie113dfd2016-10-11 10:20:12 -07001771 def CannotTriggerTryJobReason(self):
1772 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1773 raise NotImplementedError()
1774
tandriide281ae2016-10-12 06:02:30 -07001775 def GetIssueOwner(self):
1776 raise NotImplementedError()
1777
tandrii8c5a3532016-11-04 07:52:02 -07001778 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001779 raise NotImplementedError()
1780
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001781
1782class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001783 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001784 super(_RietveldChangelistImpl, self).__init__(changelist)
1785 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001786 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001787 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001788
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001789 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001790 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001791 self._props = None
1792 self._rpc_server = None
1793
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001794 def GetCodereviewServer(self):
1795 if not self._rietveld_server:
1796 # If we're on a branch then get the server potentially associated
1797 # with that branch.
1798 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001799 self._rietveld_server = gclient_utils.UpgradeToHttps(
1800 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001801 if not self._rietveld_server:
1802 self._rietveld_server = settings.GetDefaultServerUrl()
1803 return self._rietveld_server
1804
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001805 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001806 """Best effort check that user is authenticated with Rietveld server."""
1807 if self._auth_config.use_oauth2:
1808 authenticator = auth.get_authenticator_for_host(
1809 self.GetCodereviewServer(), self._auth_config)
1810 if not authenticator.has_cached_credentials():
1811 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001812 if refresh:
1813 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001814
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001815 def FetchDescription(self):
1816 issue = self.GetIssue()
1817 assert issue
1818 try:
1819 return self.RpcServer().get_description(issue).strip()
1820 except urllib2.HTTPError as e:
1821 if e.code == 404:
1822 DieWithError(
1823 ('\nWhile fetching the description for issue %d, received a '
1824 '404 (not found)\n'
1825 'error. It is likely that you deleted this '
1826 'issue on the server. If this is the\n'
1827 'case, please run\n\n'
1828 ' git cl issue 0\n\n'
1829 'to clear the association with the deleted issue. Then run '
1830 'this command again.') % issue)
1831 else:
1832 DieWithError(
1833 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1834 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001835 print('Warning: Failed to retrieve CL description due to network '
1836 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001837 return ''
1838
1839 def GetMostRecentPatchset(self):
1840 return self.GetIssueProperties()['patchsets'][-1]
1841
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842 def GetIssueProperties(self):
1843 if self._props is None:
1844 issue = self.GetIssue()
1845 if not issue:
1846 self._props = {}
1847 else:
1848 self._props = self.RpcServer().get_issue_properties(issue, True)
1849 return self._props
1850
tandriie113dfd2016-10-11 10:20:12 -07001851 def CannotTriggerTryJobReason(self):
1852 props = self.GetIssueProperties()
1853 if not props:
1854 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1855 if props.get('closed'):
1856 return 'CL %s is closed' % self.GetIssue()
1857 if props.get('private'):
1858 return 'CL %s is private' % self.GetIssue()
1859 return None
1860
tandrii8c5a3532016-11-04 07:52:02 -07001861 def GetTryjobProperties(self, patchset=None):
1862 """Returns dictionary of properties to launch tryjob."""
1863 project = (self.GetIssueProperties() or {}).get('project')
1864 return {
1865 'issue': self.GetIssue(),
1866 'patch_project': project,
1867 'patch_storage': 'rietveld',
1868 'patchset': patchset or self.GetPatchset(),
1869 'rietveld': self.GetCodereviewServer(),
1870 }
1871
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001872 def GetApprovingReviewers(self):
1873 return get_approving_reviewers(self.GetIssueProperties())
1874
tandriide281ae2016-10-12 06:02:30 -07001875 def GetIssueOwner(self):
1876 return (self.GetIssueProperties() or {}).get('owner_email')
1877
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001878 def AddComment(self, message):
1879 return self.RpcServer().add_comment(self.GetIssue(), message)
1880
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001881 def GetStatus(self):
1882 """Apply a rough heuristic to give a simple summary of an issue's review
1883 or CQ status, assuming adherence to a common workflow.
1884
1885 Returns None if no issue for this branch, or one of the following keywords:
1886 * 'error' - error from review tool (including deleted issues)
1887 * 'unsent' - not sent for review
1888 * 'waiting' - waiting for review
1889 * 'reply' - waiting for owner to reply to review
1890 * 'lgtm' - LGTM from at least one approved reviewer
1891 * 'commit' - in the commit queue
1892 * 'closed' - closed
1893 """
1894 if not self.GetIssue():
1895 return None
1896
1897 try:
1898 props = self.GetIssueProperties()
1899 except urllib2.HTTPError:
1900 return 'error'
1901
1902 if props.get('closed'):
1903 # Issue is closed.
1904 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001905 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001906 # Issue is in the commit queue.
1907 return 'commit'
1908
1909 try:
1910 reviewers = self.GetApprovingReviewers()
1911 except urllib2.HTTPError:
1912 return 'error'
1913
1914 if reviewers:
1915 # Was LGTM'ed.
1916 return 'lgtm'
1917
1918 messages = props.get('messages') or []
1919
tandrii9d2c7a32016-06-22 03:42:45 -07001920 # Skip CQ messages that don't require owner's action.
1921 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1922 if 'Dry run:' in messages[-1]['text']:
1923 messages.pop()
1924 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1925 # This message always follows prior messages from CQ,
1926 # so skip this too.
1927 messages.pop()
1928 else:
1929 # This is probably a CQ messages warranting user attention.
1930 break
1931
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001932 if not messages:
1933 # No message was sent.
1934 return 'unsent'
1935 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001936 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001937 return 'reply'
1938 return 'waiting'
1939
dsansomee2d6fd92016-09-08 00:10:47 -07001940 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001941 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001942
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001944 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001945
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001946 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001947 return self.SetFlags({flag: value})
1948
1949 def SetFlags(self, flags):
1950 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001951 """
phajdan.jr68598232016-08-10 03:28:28 -07001952 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001953 try:
tandrii4b233bd2016-07-06 03:50:29 -07001954 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001955 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001956 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001957 if e.code == 404:
1958 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1959 if e.code == 403:
1960 DieWithError(
1961 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001962 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001963 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001964
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001965 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001966 """Returns an upload.RpcServer() to access this review's rietveld instance.
1967 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001968 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001969 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001970 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001971 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001972 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001974 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001975 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001976 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001977
tandrii5d48c322016-08-18 16:19:37 -07001978 @classmethod
1979 def PatchsetConfigKey(cls):
1980 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001981
tandrii5d48c322016-08-18 16:19:37 -07001982 @classmethod
1983 def CodereviewServerConfigKey(cls):
1984 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001985
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001986 def GetRieveldObjForPresubmit(self):
1987 return self.RpcServer()
1988
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001989 def SetCQState(self, new_state):
1990 props = self.GetIssueProperties()
1991 if props.get('private'):
1992 DieWithError('Cannot set-commit on private issue')
1993
1994 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001995 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001996 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001997 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001998 else:
tandrii4b233bd2016-07-06 03:50:29 -07001999 assert new_state == _CQState.DRY_RUN
2000 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002001
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002002 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2003 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002004 # PatchIssue should never be called with a dirty tree. It is up to the
2005 # caller to check this, but just in case we assert here since the
2006 # consequences of the caller not checking this could be dire.
2007 assert(not git_common.is_dirty_git_tree('apply'))
2008 assert(parsed_issue_arg.valid)
2009 self._changelist.issue = parsed_issue_arg.issue
2010 if parsed_issue_arg.hostname:
2011 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2012
skobes6468b902016-10-24 08:45:10 -07002013 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2014 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2015 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002016 try:
skobes6468b902016-10-24 08:45:10 -07002017 scm_obj.apply_patch(patchset_object)
2018 except Exception as e:
2019 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002020 return 1
2021
2022 # If we had an issue, commit the current state and register the issue.
2023 if not nocommit:
2024 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2025 'patch from issue %(i)s at patchset '
2026 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2027 % {'i': self.GetIssue(), 'p': patchset})])
2028 self.SetIssue(self.GetIssue())
2029 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002030 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002031 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002032 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002033 return 0
2034
2035 @staticmethod
2036 def ParseIssueURL(parsed_url):
2037 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2038 return None
wychen3c1c1722016-08-04 11:46:36 -07002039 # Rietveld patch: https://domain/<number>/#ps<patchset>
2040 match = re.match(r'/(\d+)/$', parsed_url.path)
2041 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2042 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002043 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002044 issue=int(match.group(1)),
2045 patchset=int(match2.group(1)),
2046 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002047 # Typical url: https://domain/<issue_number>[/[other]]
2048 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2049 if match:
skobes6468b902016-10-24 08:45:10 -07002050 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002051 issue=int(match.group(1)),
2052 hostname=parsed_url.netloc)
2053 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2054 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2055 if match:
skobes6468b902016-10-24 08:45:10 -07002056 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002057 issue=int(match.group(1)),
2058 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002059 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002060 return None
2061
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002062 def CMDUploadChange(self, options, args, change):
2063 """Upload the patch to Rietveld."""
2064 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2065 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002066 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2067 if options.emulate_svn_auto_props:
2068 upload_args.append('--emulate_svn_auto_props')
2069
2070 change_desc = None
2071
2072 if options.email is not None:
2073 upload_args.extend(['--email', options.email])
2074
2075 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002076 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002077 upload_args.extend(['--title', options.title])
2078 if options.message:
2079 upload_args.extend(['--message', options.message])
2080 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002081 print('This branch is associated with issue %s. '
2082 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002083 else:
nodirca166002016-06-27 10:59:51 -07002084 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002085 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002086 if options.message:
2087 message = options.message
2088 else:
2089 message = CreateDescriptionFromLog(args)
2090 if options.title:
2091 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002092 change_desc = ChangeDescription(message)
2093 if options.reviewers or options.tbr_owners:
2094 change_desc.update_reviewers(options.reviewers,
2095 options.tbr_owners,
2096 change)
2097 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002098 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002099
2100 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002101 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002102 return 1
2103
2104 upload_args.extend(['--message', change_desc.description])
2105 if change_desc.get_reviewers():
2106 upload_args.append('--reviewers=%s' % ','.join(
2107 change_desc.get_reviewers()))
2108 if options.send_mail:
2109 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002110 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002111 upload_args.append('--send_mail')
2112
2113 # We check this before applying rietveld.private assuming that in
2114 # rietveld.cc only addresses which we can send private CLs to are listed
2115 # if rietveld.private is set, and so we should ignore rietveld.cc only
2116 # when --private is specified explicitly on the command line.
2117 if options.private:
2118 logging.warn('rietveld.cc is ignored since private flag is specified. '
2119 'You need to review and add them manually if necessary.')
2120 cc = self.GetCCListWithoutDefault()
2121 else:
2122 cc = self.GetCCList()
2123 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002124 if change_desc.get_cced():
2125 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002126 if cc:
2127 upload_args.extend(['--cc', cc])
2128
2129 if options.private or settings.GetDefaultPrivateFlag() == "True":
2130 upload_args.append('--private')
2131
2132 upload_args.extend(['--git_similarity', str(options.similarity)])
2133 if not options.find_copies:
2134 upload_args.extend(['--git_no_find_copies'])
2135
2136 # Include the upstream repo's URL in the change -- this is useful for
2137 # projects that have their source spread across multiple repos.
2138 remote_url = self.GetGitBaseUrlFromConfig()
2139 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002140 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2141 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2142 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002143 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002144 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002145 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002146 if target_ref:
2147 upload_args.extend(['--target_ref', target_ref])
2148
2149 # Look for dependent patchsets. See crbug.com/480453 for more details.
2150 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2151 upstream_branch = ShortBranchName(upstream_branch)
2152 if remote is '.':
2153 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002154 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002155 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002156 print()
2157 print('Skipping dependency patchset upload because git config '
2158 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2159 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002160 else:
2161 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002162 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002163 auth_config=auth_config)
2164 branch_cl_issue_url = branch_cl.GetIssueURL()
2165 branch_cl_issue = branch_cl.GetIssue()
2166 branch_cl_patchset = branch_cl.GetPatchset()
2167 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2168 upload_args.extend(
2169 ['--depends_on_patchset', '%s:%s' % (
2170 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002171 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002172 '\n'
2173 'The current branch (%s) is tracking a local branch (%s) with '
2174 'an associated CL.\n'
2175 'Adding %s/#ps%s as a dependency patchset.\n'
2176 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2177 branch_cl_patchset))
2178
2179 project = settings.GetProject()
2180 if project:
2181 upload_args.extend(['--project', project])
2182
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002183 try:
2184 upload_args = ['upload'] + upload_args + args
2185 logging.info('upload.RealMain(%s)', upload_args)
2186 issue, patchset = upload.RealMain(upload_args)
2187 issue = int(issue)
2188 patchset = int(patchset)
2189 except KeyboardInterrupt:
2190 sys.exit(1)
2191 except:
2192 # If we got an exception after the user typed a description for their
2193 # change, back up the description before re-raising.
2194 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002195 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002196 raise
2197
2198 if not self.GetIssue():
2199 self.SetIssue(issue)
2200 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002201 return 0
2202
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002203
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002204class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002205 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002206 # auth_config is Rietveld thing, kept here to preserve interface only.
2207 super(_GerritChangelistImpl, self).__init__(changelist)
2208 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002209 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002210 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002211 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002212 # Map from change number (issue) to its detail cache.
2213 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002214
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002215 if codereview_host is not None:
2216 assert not codereview_host.startswith('https://'), codereview_host
2217 self._gerrit_host = codereview_host
2218 self._gerrit_server = 'https://%s' % codereview_host
2219
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002220 def _GetGerritHost(self):
2221 # Lazy load of configs.
2222 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002223 if self._gerrit_host and '.' not in self._gerrit_host:
2224 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2225 # This happens for internal stuff http://crbug.com/614312.
2226 parsed = urlparse.urlparse(self.GetRemoteUrl())
2227 if parsed.scheme == 'sso':
2228 print('WARNING: using non https URLs for remote is likely broken\n'
2229 ' Your current remote is: %s' % self.GetRemoteUrl())
2230 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2231 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002232 return self._gerrit_host
2233
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002234 def _GetGitHost(self):
2235 """Returns git host to be used when uploading change to Gerrit."""
2236 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2237
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002238 def GetCodereviewServer(self):
2239 if not self._gerrit_server:
2240 # If we're on a branch then get the server potentially associated
2241 # with that branch.
2242 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002243 self._gerrit_server = self._GitGetBranchConfigValue(
2244 self.CodereviewServerConfigKey())
2245 if self._gerrit_server:
2246 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002247 if not self._gerrit_server:
2248 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2249 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002250 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002251 parts[0] = parts[0] + '-review'
2252 self._gerrit_host = '.'.join(parts)
2253 self._gerrit_server = 'https://%s' % self._gerrit_host
2254 return self._gerrit_server
2255
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002256 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002257 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002258 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002259
tandrii5d48c322016-08-18 16:19:37 -07002260 @classmethod
2261 def PatchsetConfigKey(cls):
2262 return 'gerritpatchset'
2263
2264 @classmethod
2265 def CodereviewServerConfigKey(cls):
2266 return 'gerritserver'
2267
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002268 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002269 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002270 if settings.GetGerritSkipEnsureAuthenticated():
2271 # For projects with unusual authentication schemes.
2272 # See http://crbug.com/603378.
2273 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002274 # Lazy-loader to identify Gerrit and Git hosts.
2275 if gerrit_util.GceAuthenticator.is_gce():
2276 return
2277 self.GetCodereviewServer()
2278 git_host = self._GetGitHost()
2279 assert self._gerrit_server and self._gerrit_host
2280 cookie_auth = gerrit_util.CookiesAuthenticator()
2281
2282 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2283 git_auth = cookie_auth.get_auth_header(git_host)
2284 if gerrit_auth and git_auth:
2285 if gerrit_auth == git_auth:
2286 return
2287 print((
2288 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2289 ' Check your %s or %s file for credentials of hosts:\n'
2290 ' %s\n'
2291 ' %s\n'
2292 ' %s') %
2293 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2294 git_host, self._gerrit_host,
2295 cookie_auth.get_new_password_message(git_host)))
2296 if not force:
2297 ask_for_data('If you know what you are doing, press Enter to continue, '
2298 'Ctrl+C to abort.')
2299 return
2300 else:
2301 missing = (
2302 [] if gerrit_auth else [self._gerrit_host] +
2303 [] if git_auth else [git_host])
2304 DieWithError('Credentials for the following hosts are required:\n'
2305 ' %s\n'
2306 'These are read from %s (or legacy %s)\n'
2307 '%s' % (
2308 '\n '.join(missing),
2309 cookie_auth.get_gitcookies_path(),
2310 cookie_auth.get_netrc_path(),
2311 cookie_auth.get_new_password_message(git_host)))
2312
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002313 def _PostUnsetIssueProperties(self):
2314 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002315 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002316
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002317 def GetRieveldObjForPresubmit(self):
2318 class ThisIsNotRietveldIssue(object):
2319 def __nonzero__(self):
2320 # This is a hack to make presubmit_support think that rietveld is not
2321 # defined, yet still ensure that calls directly result in a decent
2322 # exception message below.
2323 return False
2324
2325 def __getattr__(self, attr):
2326 print(
2327 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2328 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2329 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2330 'or use Rietveld for codereview.\n'
2331 'See also http://crbug.com/579160.' % attr)
2332 raise NotImplementedError()
2333 return ThisIsNotRietveldIssue()
2334
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002335 def GetGerritObjForPresubmit(self):
2336 return presubmit_support.GerritAccessor(self._GetGerritHost())
2337
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002338 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002339 """Apply a rough heuristic to give a simple summary of an issue's review
2340 or CQ status, assuming adherence to a common workflow.
2341
2342 Returns None if no issue for this branch, or one of the following keywords:
2343 * 'error' - error from review tool (including deleted issues)
2344 * 'unsent' - no reviewers added
2345 * 'waiting' - waiting for review
2346 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002347 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002348 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002349 * 'commit' - in the commit queue
2350 * 'closed' - abandoned
2351 """
2352 if not self.GetIssue():
2353 return None
2354
2355 try:
2356 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002357 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002358 return 'error'
2359
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002360 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002361 return 'closed'
2362
2363 cq_label = data['labels'].get('Commit-Queue', {})
2364 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002365 votes = cq_label.get('all', [])
2366 highest_vote = 0
2367 for v in votes:
2368 highest_vote = max(highest_vote, v.get('value', 0))
2369 vote_value = str(highest_vote)
2370 if vote_value != '0':
2371 # Add a '+' if the value is not 0 to match the values in the label.
2372 # The cq_label does not have negatives.
2373 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002374 vote_text = cq_label.get('values', {}).get(vote_value, '')
2375 if vote_text.lower() == 'commit':
2376 return 'commit'
2377
2378 lgtm_label = data['labels'].get('Code-Review', {})
2379 if lgtm_label:
2380 if 'rejected' in lgtm_label:
2381 return 'not lgtm'
2382 if 'approved' in lgtm_label:
2383 return 'lgtm'
2384
2385 if not data.get('reviewers', {}).get('REVIEWER', []):
2386 return 'unsent'
2387
2388 messages = data.get('messages', [])
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002389 owner = data['owner'].get('_account_id')
2390 while messages:
2391 last_message_author = messages.pop().get('author', {})
2392 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2393 # Ignore replies from CQ.
2394 continue
Andrii Shyshkalov7afd1642017-01-29 16:07:15 +01002395 if last_message_author.get('_account_id') != owner:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002396 # Some reply from non-owner.
2397 return 'reply'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002398 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002399
2400 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002401 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002402 return data['revisions'][data['current_revision']]['_number']
2403
2404 def FetchDescription(self):
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002405 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002406 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002407 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002408
dsansomee2d6fd92016-09-08 00:10:47 -07002409 def UpdateDescriptionRemote(self, description, force=False):
2410 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2411 if not force:
2412 ask_for_data(
2413 'The description cannot be modified while the issue has a pending '
2414 'unpublished edit. Either publish the edit in the Gerrit web UI '
2415 'or delete it.\n\n'
2416 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2417
2418 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2419 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002420 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002421 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002422
2423 def CloseIssue(self):
2424 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2425
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002426 def GetApprovingReviewers(self):
2427 """Returns a list of reviewers approving the change.
2428
2429 Note: not necessarily committers.
2430 """
2431 raise NotImplementedError()
2432
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002433 def SubmitIssue(self, wait_for_merge=True):
2434 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2435 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002436
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002437 def _GetChangeDetail(self, options=None, issue=None,
2438 no_cache=False):
2439 """Returns details of the issue by querying Gerrit and caching results.
2440
2441 If fresh data is needed, set no_cache=True which will clear cache and
2442 thus new data will be fetched from Gerrit.
2443 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002444 options = options or []
2445 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002446 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002447
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002448 # Optimization to avoid multiple RPCs:
2449 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2450 'CURRENT_COMMIT' not in options):
2451 options.append('CURRENT_COMMIT')
2452
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002453 # Normalize issue and options for consistent keys in cache.
2454 issue = str(issue)
2455 options = [o.upper() for o in options]
2456
2457 # Check in cache first unless no_cache is True.
2458 if no_cache:
2459 self._detail_cache.pop(issue, None)
2460 else:
2461 options_set = frozenset(options)
2462 for cached_options_set, data in self._detail_cache.get(issue, []):
2463 # Assumption: data fetched before with extra options is suitable
2464 # for return for a smaller set of options.
2465 # For example, if we cached data for
2466 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2467 # and request is for options=[CURRENT_REVISION],
2468 # THEN we can return prior cached data.
2469 if options_set.issubset(cached_options_set):
2470 return data
2471
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002472 try:
2473 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2474 options, ignore_404=False)
2475 except gerrit_util.GerritError as e:
2476 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002477 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002478 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002479
2480 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002481 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002482
agable32978d92016-11-01 12:55:02 -07002483 def _GetChangeCommit(self, issue=None):
2484 issue = issue or self.GetIssue()
2485 assert issue, 'issue is required to query Gerrit'
2486 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2487 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002488 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002489 return data
2490
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002491 def CMDLand(self, force, bypass_hooks, verbose):
2492 if git_common.is_dirty_git_tree('land'):
2493 return 1
tandriid60367b2016-06-22 05:25:12 -07002494 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2495 if u'Commit-Queue' in detail.get('labels', {}):
2496 if not force:
2497 ask_for_data('\nIt seems this repository has a Commit Queue, '
2498 'which can test and land changes for you. '
2499 'Are you sure you wish to bypass it?\n'
2500 'Press Enter to continue, Ctrl+C to abort.')
2501
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002502 differs = True
tandriic4344b52016-08-29 06:04:54 -07002503 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002504 # Note: git diff outputs nothing if there is no diff.
2505 if not last_upload or RunGit(['diff', last_upload]).strip():
2506 print('WARNING: some changes from local branch haven\'t been uploaded')
2507 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002508 if detail['current_revision'] == last_upload:
2509 differs = False
2510 else:
2511 print('WARNING: local branch contents differ from latest uploaded '
2512 'patchset')
2513 if differs:
2514 if not force:
2515 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002516 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2517 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002518 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2519 elif not bypass_hooks:
2520 hook_results = self.RunHook(
2521 committing=True,
2522 may_prompt=not force,
2523 verbose=verbose,
2524 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2525 if not hook_results.should_continue():
2526 return 1
2527
2528 self.SubmitIssue(wait_for_merge=True)
2529 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002530 links = self._GetChangeCommit().get('web_links', [])
2531 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002532 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002533 print('Landed as %s' % link.get('url'))
2534 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002535 return 0
2536
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002537 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2538 directory):
2539 assert not reject
2540 assert not nocommit
2541 assert not directory
2542 assert parsed_issue_arg.valid
2543
2544 self._changelist.issue = parsed_issue_arg.issue
2545
2546 if parsed_issue_arg.hostname:
2547 self._gerrit_host = parsed_issue_arg.hostname
2548 self._gerrit_server = 'https://%s' % self._gerrit_host
2549
tandriic2405f52016-10-10 08:13:15 -07002550 try:
2551 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002552 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002553 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002554
2555 if not parsed_issue_arg.patchset:
2556 # Use current revision by default.
2557 revision_info = detail['revisions'][detail['current_revision']]
2558 patchset = int(revision_info['_number'])
2559 else:
2560 patchset = parsed_issue_arg.patchset
2561 for revision_info in detail['revisions'].itervalues():
2562 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2563 break
2564 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002565 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002566 (parsed_issue_arg.patchset, self.GetIssue()))
2567
2568 fetch_info = revision_info['fetch']['http']
2569 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2570 RunGit(['cherry-pick', 'FETCH_HEAD'])
2571 self.SetIssue(self.GetIssue())
2572 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002573 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002574 (self.GetIssue(), self.GetPatchset()))
2575 return 0
2576
2577 @staticmethod
2578 def ParseIssueURL(parsed_url):
2579 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2580 return None
2581 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2582 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2583 # Short urls like https://domain/<issue_number> can be used, but don't allow
2584 # specifying the patchset (you'd 404), but we allow that here.
2585 if parsed_url.path == '/':
2586 part = parsed_url.fragment
2587 else:
2588 part = parsed_url.path
2589 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2590 if match:
2591 return _ParsedIssueNumberArgument(
2592 issue=int(match.group(2)),
2593 patchset=int(match.group(4)) if match.group(4) else None,
2594 hostname=parsed_url.netloc)
2595 return None
2596
tandrii16e0b4e2016-06-07 10:34:28 -07002597 def _GerritCommitMsgHookCheck(self, offer_removal):
2598 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2599 if not os.path.exists(hook):
2600 return
2601 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2602 # custom developer made one.
2603 data = gclient_utils.FileRead(hook)
2604 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2605 return
2606 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002607 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002608 'and may interfere with it in subtle ways.\n'
2609 'We recommend you remove the commit-msg hook.')
2610 if offer_removal:
2611 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2612 if reply.lower().startswith('y'):
2613 gclient_utils.rm_file_or_tree(hook)
2614 print('Gerrit commit-msg hook removed.')
2615 else:
2616 print('OK, will keep Gerrit commit-msg hook in place.')
2617
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002618 def CMDUploadChange(self, options, args, change):
2619 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002620 if options.squash and options.no_squash:
2621 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002622
2623 if not options.squash and not options.no_squash:
2624 # Load default for user, repo, squash=true, in this order.
2625 options.squash = settings.GetSquashGerritUploads()
2626 elif options.no_squash:
2627 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002628
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002629 # We assume the remote called "origin" is the one we want.
2630 # It is probably not worthwhile to support different workflows.
2631 gerrit_remote = 'origin'
2632
2633 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002634 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002635
Aaron Gableb56ad332017-01-06 15:24:31 -08002636 # This may be None; default fallback value is determined in logic below.
2637 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002638 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002639
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002640 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002641 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002642 if self.GetIssue():
2643 # Try to get the message from a previous upload.
2644 message = self.GetDescription()
2645 if not message:
2646 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002647 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002648 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002649 if not title:
2650 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2651 title = ask_for_data(
2652 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002653 if title == default_title:
2654 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002655 change_id = self._GetChangeDetail()['change_id']
2656 while True:
2657 footer_change_ids = git_footers.get_footer_change_id(message)
2658 if footer_change_ids == [change_id]:
2659 break
2660 if not footer_change_ids:
2661 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002662 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002663 continue
2664 # There is already a valid footer but with different or several ids.
2665 # Doing this automatically is non-trivial as we don't want to lose
2666 # existing other footers, yet we want to append just 1 desired
2667 # Change-Id. Thus, just create a new footer, but let user verify the
2668 # new description.
2669 message = '%s\n\nChange-Id: %s' % (message, change_id)
2670 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002671 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002672 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002673 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002674 'Please, check the proposed correction to the description, '
2675 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2676 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2677 change_id))
2678 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2679 if not options.force:
2680 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002681 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002682 message = change_desc.description
2683 if not message:
2684 DieWithError("Description is empty. Aborting...")
2685 # Continue the while loop.
2686 # Sanity check of this code - we should end up with proper message
2687 # footer.
2688 assert [change_id] == git_footers.get_footer_change_id(message)
2689 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002690 else: # if not self.GetIssue()
2691 if options.message:
2692 message = options.message
2693 else:
2694 message = CreateDescriptionFromLog(args)
2695 if options.title:
2696 message = options.title + '\n\n' + message
2697 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002698 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002699 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002700 # On first upload, patchset title is always this string, while
2701 # --title flag gets converted to first line of message.
2702 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002703 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002704 if not change_desc.description:
2705 DieWithError("Description is empty. Aborting...")
2706 message = change_desc.description
2707 change_ids = git_footers.get_footer_change_id(message)
2708 if len(change_ids) > 1:
2709 DieWithError('too many Change-Id footers, at most 1 allowed.')
2710 if not change_ids:
2711 # Generate the Change-Id automatically.
2712 message = git_footers.add_footer_change_id(
2713 message, GenerateGerritChangeId(message))
2714 change_desc.set_description(message)
2715 change_ids = git_footers.get_footer_change_id(message)
2716 assert len(change_ids) == 1
2717 change_id = change_ids[0]
2718
2719 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2720 if remote is '.':
2721 # If our upstream branch is local, we base our squashed commit on its
2722 # squashed version.
2723 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2724 # Check the squashed hash of the parent.
2725 parent = RunGit(['config',
2726 'branch.%s.gerritsquashhash' % upstream_branch_name],
2727 error_ok=True).strip()
2728 # Verify that the upstream branch has been uploaded too, otherwise
2729 # Gerrit will create additional CLs when uploading.
2730 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2731 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002732 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002733 '\nUpload upstream branch %s first.\n'
2734 'It is likely that this branch has been rebased since its last '
2735 'upload, so you just need to upload it again.\n'
2736 '(If you uploaded it with --no-squash, then branch dependencies '
2737 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002738 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002739 else:
2740 parent = self.GetCommonAncestorWithUpstream()
2741
2742 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2743 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2744 '-m', message]).strip()
2745 else:
2746 change_desc = ChangeDescription(
2747 options.message or CreateDescriptionFromLog(args))
2748 if not change_desc.description:
2749 DieWithError("Description is empty. Aborting...")
2750
2751 if not git_footers.get_footer_change_id(change_desc.description):
2752 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002753 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2754 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002755 ref_to_push = 'HEAD'
2756 parent = '%s/%s' % (gerrit_remote, branch)
2757 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2758
2759 assert change_desc
2760 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2761 ref_to_push)]).splitlines()
2762 if len(commits) > 1:
2763 print('WARNING: This will upload %d commits. Run the following command '
2764 'to see which commits will be uploaded: ' % len(commits))
2765 print('git log %s..%s' % (parent, ref_to_push))
2766 print('You can also use `git squash-branch` to squash these into a '
2767 'single commit.')
2768 ask_for_data('About to upload; enter to confirm.')
2769
2770 if options.reviewers or options.tbr_owners:
2771 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2772 change)
2773
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002774 # Extra options that can be specified at push time. Doc:
2775 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2776 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002777 if change_desc.get_reviewers(tbr_only=True):
2778 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2779 refspec_opts.append('l=Code-Review+1')
2780
Aaron Gable9b713dd2016-12-14 16:04:21 -08002781 if title:
2782 if not re.match(r'^[\w ]+$', title):
2783 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002784 if not automatic_title:
2785 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gableeb3af712017-02-02 12:42:02 -08002786 'and spaces. You can edit it in the UI. '
2787 'See https://crbug.com/663787.\n'
2788 'Cleaned up title: %s' % title)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002789 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2790 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002791 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002792
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002793 if options.send_mail:
2794 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002795 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002796 refspec_opts.append('notify=ALL')
2797 else:
2798 refspec_opts.append('notify=NONE')
2799
tandrii99a72f22016-08-17 14:33:24 -07002800 reviewers = change_desc.get_reviewers()
2801 if reviewers:
2802 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002803
agablec6787972016-09-09 16:13:34 -07002804 if options.private:
2805 refspec_opts.append('draft')
2806
rmistry9eadede2016-09-19 11:22:43 -07002807 if options.topic:
2808 # Documentation on Gerrit topics is here:
2809 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2810 refspec_opts.append('topic=%s' % options.topic)
2811
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002812 refspec_suffix = ''
2813 if refspec_opts:
2814 refspec_suffix = '%' + ','.join(refspec_opts)
2815 assert ' ' not in refspec_suffix, (
2816 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002817 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002818
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002819 try:
2820 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalove9c78ff2017-02-06 15:53:13 +01002821 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002822 print_stdout=True,
2823 # Flush after every line: useful for seeing progress when running as
2824 # recipe.
2825 filter_fn=lambda _: sys.stdout.flush())
2826 except subprocess2.CalledProcessError:
2827 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002828 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002829
2830 if options.squash:
2831 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2832 change_numbers = [m.group(1)
2833 for m in map(regex.match, push_stdout.splitlines())
2834 if m]
2835 if len(change_numbers) != 1:
2836 DieWithError(
2837 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002838 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002839 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002840 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002841
2842 # Add cc's from the CC_LIST and --cc flag (if any).
2843 cc = self.GetCCList().split(',')
2844 if options.cc:
2845 cc.extend(options.cc)
2846 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002847 if change_desc.get_cced():
2848 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002849 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002850 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002851 self._GetGerritHost(), self.GetIssue(), cc,
2852 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002853 return 0
2854
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002855 def _AddChangeIdToCommitMessage(self, options, args):
2856 """Re-commits using the current message, assumes the commit hook is in
2857 place.
2858 """
2859 log_desc = options.message or CreateDescriptionFromLog(args)
2860 git_command = ['commit', '--amend', '-m', log_desc]
2861 RunGit(git_command)
2862 new_log_desc = CreateDescriptionFromLog(args)
2863 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002864 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002865 return new_log_desc
2866 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002867 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002868
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002869 def SetCQState(self, new_state):
2870 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002871 vote_map = {
2872 _CQState.NONE: 0,
2873 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002874 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002875 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002876 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2877 if new_state == _CQState.DRY_RUN:
2878 # Don't spam everybody reviewer/owner.
2879 kwargs['notify'] = 'NONE'
2880 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002881
tandriie113dfd2016-10-11 10:20:12 -07002882 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002883 try:
2884 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002885 except GerritChangeNotExists:
2886 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002887
2888 if data['status'] in ('ABANDONED', 'MERGED'):
2889 return 'CL %s is closed' % self.GetIssue()
2890
2891 def GetTryjobProperties(self, patchset=None):
2892 """Returns dictionary of properties to launch tryjob."""
2893 data = self._GetChangeDetail(['ALL_REVISIONS'])
2894 patchset = int(patchset or self.GetPatchset())
2895 assert patchset
2896 revision_data = None # Pylint wants it to be defined.
2897 for revision_data in data['revisions'].itervalues():
2898 if int(revision_data['_number']) == patchset:
2899 break
2900 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002901 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002902 (patchset, self.GetIssue()))
2903 return {
2904 'patch_issue': self.GetIssue(),
2905 'patch_set': patchset or self.GetPatchset(),
2906 'patch_project': data['project'],
2907 'patch_storage': 'gerrit',
2908 'patch_ref': revision_data['fetch']['http']['ref'],
2909 'patch_repository_url': revision_data['fetch']['http']['url'],
2910 'patch_gerrit_url': self.GetCodereviewServer(),
2911 }
tandriie113dfd2016-10-11 10:20:12 -07002912
tandriide281ae2016-10-12 06:02:30 -07002913 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002914 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002915
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002916
2917_CODEREVIEW_IMPLEMENTATIONS = {
2918 'rietveld': _RietveldChangelistImpl,
2919 'gerrit': _GerritChangelistImpl,
2920}
2921
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002922
iannuccie53c9352016-08-17 14:40:40 -07002923def _add_codereview_issue_select_options(parser, extra=""):
2924 _add_codereview_select_options(parser)
2925
2926 text = ('Operate on this issue number instead of the current branch\'s '
2927 'implicit issue.')
2928 if extra:
2929 text += ' '+extra
2930 parser.add_option('-i', '--issue', type=int, help=text)
2931
2932
2933def _process_codereview_issue_select_options(parser, options):
2934 _process_codereview_select_options(parser, options)
2935 if options.issue is not None and not options.forced_codereview:
2936 parser.error('--issue must be specified with either --rietveld or --gerrit')
2937
2938
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002939def _add_codereview_select_options(parser):
2940 """Appends --gerrit and --rietveld options to force specific codereview."""
2941 parser.codereview_group = optparse.OptionGroup(
2942 parser, 'EXPERIMENTAL! Codereview override options')
2943 parser.add_option_group(parser.codereview_group)
2944 parser.codereview_group.add_option(
2945 '--gerrit', action='store_true',
2946 help='Force the use of Gerrit for codereview')
2947 parser.codereview_group.add_option(
2948 '--rietveld', action='store_true',
2949 help='Force the use of Rietveld for codereview')
2950
2951
2952def _process_codereview_select_options(parser, options):
2953 if options.gerrit and options.rietveld:
2954 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2955 options.forced_codereview = None
2956 if options.gerrit:
2957 options.forced_codereview = 'gerrit'
2958 elif options.rietveld:
2959 options.forced_codereview = 'rietveld'
2960
2961
tandriif9aefb72016-07-01 09:06:51 -07002962def _get_bug_line_values(default_project, bugs):
2963 """Given default_project and comma separated list of bugs, yields bug line
2964 values.
2965
2966 Each bug can be either:
2967 * a number, which is combined with default_project
2968 * string, which is left as is.
2969
2970 This function may produce more than one line, because bugdroid expects one
2971 project per line.
2972
2973 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2974 ['v8:123', 'chromium:789']
2975 """
2976 default_bugs = []
2977 others = []
2978 for bug in bugs.split(','):
2979 bug = bug.strip()
2980 if bug:
2981 try:
2982 default_bugs.append(int(bug))
2983 except ValueError:
2984 others.append(bug)
2985
2986 if default_bugs:
2987 default_bugs = ','.join(map(str, default_bugs))
2988 if default_project:
2989 yield '%s:%s' % (default_project, default_bugs)
2990 else:
2991 yield default_bugs
2992 for other in sorted(others):
2993 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2994 yield other
2995
2996
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002997class ChangeDescription(object):
2998 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002999 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003000 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003001 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003002 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003003
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003004 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003005 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003006
agable@chromium.org42c20792013-09-12 17:34:49 +00003007 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003008 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003009 return '\n'.join(self._description_lines)
3010
3011 def set_description(self, desc):
3012 if isinstance(desc, basestring):
3013 lines = desc.splitlines()
3014 else:
3015 lines = [line.rstrip() for line in desc]
3016 while lines and not lines[0]:
3017 lines.pop(0)
3018 while lines and not lines[-1]:
3019 lines.pop(-1)
3020 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003021
piman@chromium.org336f9122014-09-04 02:16:55 +00003022 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003023 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003024 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003025 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003026 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003027 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003028
agable@chromium.org42c20792013-09-12 17:34:49 +00003029 # Get the set of R= and TBR= lines and remove them from the desciption.
3030 regexp = re.compile(self.R_LINE)
3031 matches = [regexp.match(line) for line in self._description_lines]
3032 new_desc = [l for i, l in enumerate(self._description_lines)
3033 if not matches[i]]
3034 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003035
agable@chromium.org42c20792013-09-12 17:34:49 +00003036 # Construct new unified R= and TBR= lines.
3037 r_names = []
3038 tbr_names = []
3039 for match in matches:
3040 if not match:
3041 continue
3042 people = cleanup_list([match.group(2).strip()])
3043 if match.group(1) == 'TBR':
3044 tbr_names.extend(people)
3045 else:
3046 r_names.extend(people)
3047 for name in r_names:
3048 if name not in reviewers:
3049 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003050 if add_owners_tbr:
3051 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003052 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003053 all_reviewers = set(tbr_names + reviewers)
3054 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3055 all_reviewers)
3056 tbr_names.extend(owners_db.reviewers_for(missing_files,
3057 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003058 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3059 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3060
3061 # Put the new lines in the description where the old first R= line was.
3062 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3063 if 0 <= line_loc < len(self._description_lines):
3064 if new_tbr_line:
3065 self._description_lines.insert(line_loc, new_tbr_line)
3066 if new_r_line:
3067 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003068 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003069 if new_r_line:
3070 self.append_footer(new_r_line)
3071 if new_tbr_line:
3072 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003073
tandriif9aefb72016-07-01 09:06:51 -07003074 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003075 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003076 self.set_description([
3077 '# Enter a description of the change.',
3078 '# This will be displayed on the codereview site.',
3079 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003080 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003081 '--------------------',
3082 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003083
agable@chromium.org42c20792013-09-12 17:34:49 +00003084 regexp = re.compile(self.BUG_LINE)
3085 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003086 prefix = settings.GetBugPrefix()
3087 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3088 for value in values:
3089 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3090 self.append_footer('BUG=%s' % value)
3091
agable@chromium.org42c20792013-09-12 17:34:49 +00003092 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003093 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003094 if not content:
3095 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003096 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003097
3098 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003099 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3100 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003101 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003102 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003103
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003104 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003105 """Adds a footer line to the description.
3106
3107 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3108 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3109 that Gerrit footers are always at the end.
3110 """
3111 parsed_footer_line = git_footers.parse_footer(line)
3112 if parsed_footer_line:
3113 # Line is a gerrit footer in the form: Footer-Key: any value.
3114 # Thus, must be appended observing Gerrit footer rules.
3115 self.set_description(
3116 git_footers.add_footer(self.description,
3117 key=parsed_footer_line[0],
3118 value=parsed_footer_line[1]))
3119 return
3120
3121 if not self._description_lines:
3122 self._description_lines.append(line)
3123 return
3124
3125 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3126 if gerrit_footers:
3127 # git_footers.split_footers ensures that there is an empty line before
3128 # actual (gerrit) footers, if any. We have to keep it that way.
3129 assert top_lines and top_lines[-1] == ''
3130 top_lines, separator = top_lines[:-1], top_lines[-1:]
3131 else:
3132 separator = [] # No need for separator if there are no gerrit_footers.
3133
3134 prev_line = top_lines[-1] if top_lines else ''
3135 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3136 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3137 top_lines.append('')
3138 top_lines.append(line)
3139 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003140
tandrii99a72f22016-08-17 14:33:24 -07003141 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003142 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003143 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003144 reviewers = [match.group(2).strip()
3145 for match in matches
3146 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003147 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003148
bradnelsond975b302016-10-23 12:20:23 -07003149 def get_cced(self):
3150 """Retrieves the list of reviewers."""
3151 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3152 cced = [match.group(2).strip() for match in matches if match]
3153 return cleanup_list(cced)
3154
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003155 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3156 """Updates this commit description given the parent.
3157
3158 This is essentially what Gnumbd used to do.
3159 Consult https://goo.gl/WMmpDe for more details.
3160 """
3161 assert parent_msg # No, orphan branch creation isn't supported.
3162 assert parent_hash
3163 assert dest_ref
3164 parent_footer_map = git_footers.parse_footers(parent_msg)
3165 # This will also happily parse svn-position, which GnumbD is no longer
3166 # supporting. While we'd generate correct footers, the verifier plugin
3167 # installed in Gerrit will block such commit (ie git push below will fail).
3168 parent_position = git_footers.get_position(parent_footer_map)
3169
3170 # Cherry-picks may have last line obscuring their prior footers,
3171 # from git_footers perspective. This is also what Gnumbd did.
3172 cp_line = None
3173 if (self._description_lines and
3174 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3175 cp_line = self._description_lines.pop()
3176
3177 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3178
3179 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3180 # user interference with actual footers we'd insert below.
3181 for i, (k, v) in enumerate(parsed_footers):
3182 if k.startswith('Cr-'):
3183 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3184
3185 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003186 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003187 if parent_position[0] == dest_ref:
3188 # Same branch as parent.
3189 number = int(parent_position[1]) + 1
3190 else:
3191 number = 1 # New branch, and extra lineage.
3192 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3193 int(parent_position[1])))
3194
3195 parsed_footers.append(('Cr-Commit-Position',
3196 '%s@{#%d}' % (dest_ref, number)))
3197 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3198
3199 self._description_lines = top_lines
3200 if cp_line:
3201 self._description_lines.append(cp_line)
3202 if self._description_lines[-1] != '':
3203 self._description_lines.append('') # Ensure footer separator.
3204 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3205
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003206
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003207def get_approving_reviewers(props):
3208 """Retrieves the reviewers that approved a CL from the issue properties with
3209 messages.
3210
3211 Note that the list may contain reviewers that are not committer, thus are not
3212 considered by the CQ.
3213 """
3214 return sorted(
3215 set(
3216 message['sender']
3217 for message in props['messages']
3218 if message['approval'] and message['sender'] in props['reviewers']
3219 )
3220 )
3221
3222
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003223def FindCodereviewSettingsFile(filename='codereview.settings'):
3224 """Finds the given file starting in the cwd and going up.
3225
3226 Only looks up to the top of the repository unless an
3227 'inherit-review-settings-ok' file exists in the root of the repository.
3228 """
3229 inherit_ok_file = 'inherit-review-settings-ok'
3230 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003231 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3233 root = '/'
3234 while True:
3235 if filename in os.listdir(cwd):
3236 if os.path.isfile(os.path.join(cwd, filename)):
3237 return open(os.path.join(cwd, filename))
3238 if cwd == root:
3239 break
3240 cwd = os.path.dirname(cwd)
3241
3242
3243def LoadCodereviewSettingsFromFile(fileobj):
3244 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003245 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003247 def SetProperty(name, setting, unset_error_ok=False):
3248 fullname = 'rietveld.' + name
3249 if setting in keyvals:
3250 RunGit(['config', fullname, keyvals[setting]])
3251 else:
3252 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3253
tandrii48df5812016-10-17 03:55:37 -07003254 if not keyvals.get('GERRIT_HOST', False):
3255 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003256 # Only server setting is required. Other settings can be absent.
3257 # In that case, we ignore errors raised during option deletion attempt.
3258 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003259 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003260 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3261 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003262 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003263 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3264 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003265 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003266 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3267 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003268
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003269 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003270 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003271
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003272 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003273 RunGit(['config', 'gerrit.squash-uploads',
3274 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003275
tandrii@chromium.org28253532016-04-14 13:46:56 +00003276 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003277 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003278 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3279
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003280 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003281 # should be of the form
3282 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3283 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003284 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3285 keyvals['ORIGIN_URL_CONFIG']])
3286
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003287
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003288def urlretrieve(source, destination):
3289 """urllib is broken for SSL connections via a proxy therefore we
3290 can't use urllib.urlretrieve()."""
3291 with open(destination, 'w') as f:
3292 f.write(urllib2.urlopen(source).read())
3293
3294
ukai@chromium.org712d6102013-11-27 00:52:58 +00003295def hasSheBang(fname):
3296 """Checks fname is a #! script."""
3297 with open(fname) as f:
3298 return f.read(2).startswith('#!')
3299
3300
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003301# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3302def DownloadHooks(*args, **kwargs):
3303 pass
3304
3305
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003306def DownloadGerritHook(force):
3307 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003308
3309 Args:
3310 force: True to update hooks. False to install hooks if not present.
3311 """
3312 if not settings.GetIsGerrit():
3313 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003314 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003315 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3316 if not os.access(dst, os.X_OK):
3317 if os.path.exists(dst):
3318 if not force:
3319 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003320 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003321 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003322 if not hasSheBang(dst):
3323 DieWithError('Not a script: %s\n'
3324 'You need to download from\n%s\n'
3325 'into .git/hooks/commit-msg and '
3326 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003327 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3328 except Exception:
3329 if os.path.exists(dst):
3330 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003331 DieWithError('\nFailed to download hooks.\n'
3332 'You need to download from\n%s\n'
3333 'into .git/hooks/commit-msg and '
3334 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003335
3336
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003337def GetRietveldCodereviewSettingsInteractively():
3338 """Prompt the user for settings."""
3339 server = settings.GetDefaultServerUrl(error_ok=True)
3340 prompt = 'Rietveld server (host[:port])'
3341 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3342 newserver = ask_for_data(prompt + ':')
3343 if not server and not newserver:
3344 newserver = DEFAULT_SERVER
3345 if newserver:
3346 newserver = gclient_utils.UpgradeToHttps(newserver)
3347 if newserver != server:
3348 RunGit(['config', 'rietveld.server', newserver])
3349
3350 def SetProperty(initial, caption, name, is_url):
3351 prompt = caption
3352 if initial:
3353 prompt += ' ("x" to clear) [%s]' % initial
3354 new_val = ask_for_data(prompt + ':')
3355 if new_val == 'x':
3356 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3357 elif new_val:
3358 if is_url:
3359 new_val = gclient_utils.UpgradeToHttps(new_val)
3360 if new_val != initial:
3361 RunGit(['config', 'rietveld.' + name, new_val])
3362
3363 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3364 SetProperty(settings.GetDefaultPrivateFlag(),
3365 'Private flag (rietveld only)', 'private', False)
3366 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3367 'tree-status-url', False)
3368 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3369 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3370 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3371 'run-post-upload-hook', False)
3372
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003373
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003374@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003375def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003376 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003377
tandrii5d0a0422016-09-14 06:24:35 -07003378 print('WARNING: git cl config works for Rietveld only')
3379 # TODO(tandrii): remove this once we switch to Gerrit.
3380 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003381 parser.add_option('--activate-update', action='store_true',
3382 help='activate auto-updating [rietveld] section in '
3383 '.git/config')
3384 parser.add_option('--deactivate-update', action='store_true',
3385 help='deactivate auto-updating [rietveld] section in '
3386 '.git/config')
3387 options, args = parser.parse_args(args)
3388
3389 if options.deactivate_update:
3390 RunGit(['config', 'rietveld.autoupdate', 'false'])
3391 return
3392
3393 if options.activate_update:
3394 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3395 return
3396
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003397 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003398 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003399 return 0
3400
3401 url = args[0]
3402 if not url.endswith('codereview.settings'):
3403 url = os.path.join(url, 'codereview.settings')
3404
3405 # Load code review settings and download hooks (if available).
3406 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3407 return 0
3408
3409
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003410def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003411 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003412 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3413 branch = ShortBranchName(branchref)
3414 _, args = parser.parse_args(args)
3415 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003416 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003417 return RunGit(['config', 'branch.%s.base-url' % branch],
3418 error_ok=False).strip()
3419 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003420 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003421 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3422 error_ok=False).strip()
3423
3424
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003425def color_for_status(status):
3426 """Maps a Changelist status to color, for CMDstatus and other tools."""
3427 return {
3428 'unsent': Fore.RED,
3429 'waiting': Fore.BLUE,
3430 'reply': Fore.YELLOW,
3431 'lgtm': Fore.GREEN,
3432 'commit': Fore.MAGENTA,
3433 'closed': Fore.CYAN,
3434 'error': Fore.WHITE,
3435 }.get(status, Fore.WHITE)
3436
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003437
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003438def get_cl_statuses(changes, fine_grained, max_processes=None):
3439 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003440
3441 If fine_grained is true, this will fetch CL statuses from the server.
3442 Otherwise, simply indicate if there's a matching url for the given branches.
3443
3444 If max_processes is specified, it is used as the maximum number of processes
3445 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3446 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003447
3448 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003449 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003450 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003451 upload.verbosity = 0
3452
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003453 if not changes:
3454 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003455
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003456 if not fine_grained:
3457 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003458 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003459 for cl in changes:
3460 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003461 return
3462
3463 # First, sort out authentication issues.
3464 logging.debug('ensuring credentials exist')
3465 for cl in changes:
3466 cl.EnsureAuthenticated(force=False, refresh=True)
3467
3468 def fetch(cl):
3469 try:
3470 return (cl, cl.GetStatus())
3471 except:
3472 # See http://crbug.com/629863.
3473 logging.exception('failed to fetch status for %s:', cl)
3474 raise
3475
3476 threads_count = len(changes)
3477 if max_processes:
3478 threads_count = max(1, min(threads_count, max_processes))
3479 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3480
3481 pool = ThreadPool(threads_count)
3482 fetched_cls = set()
3483 try:
3484 it = pool.imap_unordered(fetch, changes).__iter__()
3485 while True:
3486 try:
3487 cl, status = it.next(timeout=5)
3488 except multiprocessing.TimeoutError:
3489 break
3490 fetched_cls.add(cl)
3491 yield cl, status
3492 finally:
3493 pool.close()
3494
3495 # Add any branches that failed to fetch.
3496 for cl in set(changes) - fetched_cls:
3497 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003498
rmistry@google.com2dd99862015-06-22 12:22:18 +00003499
3500def upload_branch_deps(cl, args):
3501 """Uploads CLs of local branches that are dependents of the current branch.
3502
3503 If the local branch dependency tree looks like:
3504 test1 -> test2.1 -> test3.1
3505 -> test3.2
3506 -> test2.2 -> test3.3
3507
3508 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3509 run on the dependent branches in this order:
3510 test2.1, test3.1, test3.2, test2.2, test3.3
3511
3512 Note: This function does not rebase your local dependent branches. Use it when
3513 you make a change to the parent branch that will not conflict with its
3514 dependent branches, and you would like their dependencies updated in
3515 Rietveld.
3516 """
3517 if git_common.is_dirty_git_tree('upload-branch-deps'):
3518 return 1
3519
3520 root_branch = cl.GetBranch()
3521 if root_branch is None:
3522 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3523 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003524 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003525 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3526 'patchset dependencies without an uploaded CL.')
3527
3528 branches = RunGit(['for-each-ref',
3529 '--format=%(refname:short) %(upstream:short)',
3530 'refs/heads'])
3531 if not branches:
3532 print('No local branches found.')
3533 return 0
3534
3535 # Create a dictionary of all local branches to the branches that are dependent
3536 # on it.
3537 tracked_to_dependents = collections.defaultdict(list)
3538 for b in branches.splitlines():
3539 tokens = b.split()
3540 if len(tokens) == 2:
3541 branch_name, tracked = tokens
3542 tracked_to_dependents[tracked].append(branch_name)
3543
vapiera7fbd5a2016-06-16 09:17:49 -07003544 print()
3545 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003546 dependents = []
3547 def traverse_dependents_preorder(branch, padding=''):
3548 dependents_to_process = tracked_to_dependents.get(branch, [])
3549 padding += ' '
3550 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003551 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003552 dependents.append(dependent)
3553 traverse_dependents_preorder(dependent, padding)
3554 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003555 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003556
3557 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003558 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003559 return 0
3560
vapiera7fbd5a2016-06-16 09:17:49 -07003561 print('This command will checkout all dependent branches and run '
3562 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003563 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3564
andybons@chromium.org962f9462016-02-03 20:00:42 +00003565 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003566 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003567 args.extend(['-t', 'Updated patchset dependency'])
3568
rmistry@google.com2dd99862015-06-22 12:22:18 +00003569 # Record all dependents that failed to upload.
3570 failures = {}
3571 # Go through all dependents, checkout the branch and upload.
3572 try:
3573 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003574 print()
3575 print('--------------------------------------')
3576 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003577 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003578 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003579 try:
3580 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003581 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003582 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003583 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003584 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003585 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003586 finally:
3587 # Swap back to the original root branch.
3588 RunGit(['checkout', '-q', root_branch])
3589
vapiera7fbd5a2016-06-16 09:17:49 -07003590 print()
3591 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003592 for dependent_branch in dependents:
3593 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print(' %s : %s' % (dependent_branch, upload_status))
3595 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003596
3597 return 0
3598
3599
kmarshall3bff56b2016-06-06 18:31:47 -07003600def CMDarchive(parser, args):
3601 """Archives and deletes branches associated with closed changelists."""
3602 parser.add_option(
3603 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003604 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003605 parser.add_option(
3606 '-f', '--force', action='store_true',
3607 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003608 parser.add_option(
3609 '-d', '--dry-run', action='store_true',
3610 help='Skip the branch tagging and removal steps.')
3611 parser.add_option(
3612 '-t', '--notags', action='store_true',
3613 help='Do not tag archived branches. '
3614 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003615
3616 auth.add_auth_options(parser)
3617 options, args = parser.parse_args(args)
3618 if args:
3619 parser.error('Unsupported args: %s' % ' '.join(args))
3620 auth_config = auth.extract_auth_config_from_options(options)
3621
3622 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3623 if not branches:
3624 return 0
3625
vapiera7fbd5a2016-06-16 09:17:49 -07003626 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003627 changes = [Changelist(branchref=b, auth_config=auth_config)
3628 for b in branches.splitlines()]
3629 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3630 statuses = get_cl_statuses(changes,
3631 fine_grained=True,
3632 max_processes=options.maxjobs)
3633 proposal = [(cl.GetBranch(),
3634 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3635 for cl, status in statuses
3636 if status == 'closed']
3637 proposal.sort()
3638
3639 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003640 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003641 return 0
3642
3643 current_branch = GetCurrentBranch()
3644
vapiera7fbd5a2016-06-16 09:17:49 -07003645 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003646 if options.notags:
3647 for next_item in proposal:
3648 print(' ' + next_item[0])
3649 else:
3650 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3651 for next_item in proposal:
3652 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003653
kmarshall9249e012016-08-23 12:02:16 -07003654 # Quit now on precondition failure or if instructed by the user, either
3655 # via an interactive prompt or by command line flags.
3656 if options.dry_run:
3657 print('\nNo changes were made (dry run).\n')
3658 return 0
3659 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003660 print('You are currently on a branch \'%s\' which is associated with a '
3661 'closed codereview issue, so archive cannot proceed. Please '
3662 'checkout another branch and run this command again.' %
3663 current_branch)
3664 return 1
kmarshall9249e012016-08-23 12:02:16 -07003665 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003666 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3667 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003668 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003669 return 1
3670
3671 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003672 if not options.notags:
3673 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003674 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003675
vapiera7fbd5a2016-06-16 09:17:49 -07003676 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003677
3678 return 0
3679
3680
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003681def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003682 """Show status of changelists.
3683
3684 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003685 - Red not sent for review or broken
3686 - Blue waiting for review
3687 - Yellow waiting for you to reply to review
3688 - Green LGTM'ed
3689 - Magenta in the commit queue
3690 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003691
3692 Also see 'git cl comments'.
3693 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003694 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003695 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003696 parser.add_option('-f', '--fast', action='store_true',
3697 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003698 parser.add_option(
3699 '-j', '--maxjobs', action='store', type=int,
3700 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003701
3702 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003703 _add_codereview_issue_select_options(
3704 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003705 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003706 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003707 if args:
3708 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003709 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003710
iannuccie53c9352016-08-17 14:40:40 -07003711 if options.issue is not None and not options.field:
3712 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003713
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003714 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003715 cl = Changelist(auth_config=auth_config, issue=options.issue,
3716 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003717 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003718 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003719 elif options.field == 'id':
3720 issueid = cl.GetIssue()
3721 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003722 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003723 elif options.field == 'patch':
3724 patchset = cl.GetPatchset()
3725 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003726 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003727 elif options.field == 'status':
3728 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003729 elif options.field == 'url':
3730 url = cl.GetIssueURL()
3731 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003732 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003733 return 0
3734
3735 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3736 if not branches:
3737 print('No local branch found.')
3738 return 0
3739
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003740 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003741 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003742 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003743 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003744 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003745 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003746 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003747
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003748 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003749 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3750 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3751 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003752 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003753 c, status = output.next()
3754 branch_statuses[c.GetBranch()] = status
3755 status = branch_statuses.pop(branch)
3756 url = cl.GetIssueURL()
3757 if url and (not status or status == 'error'):
3758 # The issue probably doesn't exist anymore.
3759 url += ' (broken)'
3760
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003761 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003762 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003763 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003764 color = ''
3765 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003766 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003767 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003768 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003769 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003770
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003771
3772 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003773 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003774 print('Current branch: %s' % branch)
3775 for cl in changes:
3776 if cl.GetBranch() == branch:
3777 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003778 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003779 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003780 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003781 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003782 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003783 print('Issue description:')
3784 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003785 return 0
3786
3787
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003788def colorize_CMDstatus_doc():
3789 """To be called once in main() to add colors to git cl status help."""
3790 colors = [i for i in dir(Fore) if i[0].isupper()]
3791
3792 def colorize_line(line):
3793 for color in colors:
3794 if color in line.upper():
3795 # Extract whitespaces first and the leading '-'.
3796 indent = len(line) - len(line.lstrip(' ')) + 1
3797 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3798 return line
3799
3800 lines = CMDstatus.__doc__.splitlines()
3801 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3802
3803
phajdan.jre328cf92016-08-22 04:12:17 -07003804def write_json(path, contents):
3805 with open(path, 'w') as f:
3806 json.dump(contents, f)
3807
3808
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003809@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003810def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003811 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003812
3813 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003814 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003815 parser.add_option('-r', '--reverse', action='store_true',
3816 help='Lookup the branch(es) for the specified issues. If '
3817 'no issues are specified, all branches with mapped '
3818 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003819 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003820 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003821 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003822 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003823
dnj@chromium.org406c4402015-03-03 17:22:28 +00003824 if options.reverse:
3825 branches = RunGit(['for-each-ref', 'refs/heads',
3826 '--format=%(refname:short)']).splitlines()
3827
3828 # Reverse issue lookup.
3829 issue_branch_map = {}
3830 for branch in branches:
3831 cl = Changelist(branchref=branch)
3832 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3833 if not args:
3834 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003835 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003836 for issue in args:
3837 if not issue:
3838 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003839 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003840 print('Branch for issue number %s: %s' % (
3841 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003842 if options.json:
3843 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003844 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003845 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003846 if len(args) > 0:
3847 try:
3848 issue = int(args[0])
3849 except ValueError:
3850 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003851 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003852 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003853 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003854 if options.json:
3855 write_json(options.json, {
3856 'issue': cl.GetIssue(),
3857 'issue_url': cl.GetIssueURL(),
3858 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003859 return 0
3860
3861
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003862def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003863 """Shows or posts review comments for any changelist."""
3864 parser.add_option('-a', '--add-comment', dest='comment',
3865 help='comment to add to an issue')
3866 parser.add_option('-i', dest='issue',
3867 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003868 parser.add_option('-j', '--json-file',
3869 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003870 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003871 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003872 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003873
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003874 issue = None
3875 if options.issue:
3876 try:
3877 issue = int(options.issue)
3878 except ValueError:
3879 DieWithError('A review issue id is expected to be a number')
3880
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003881 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003882
3883 if options.comment:
3884 cl.AddComment(options.comment)
3885 return 0
3886
3887 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003888 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003889 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003890 summary.append({
3891 'date': message['date'],
3892 'lgtm': False,
3893 'message': message['text'],
3894 'not_lgtm': False,
3895 'sender': message['sender'],
3896 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003897 if message['disapproval']:
3898 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003899 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003900 elif message['approval']:
3901 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003902 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003903 elif message['sender'] == data['owner_email']:
3904 color = Fore.MAGENTA
3905 else:
3906 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003907 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003908 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003909 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003910 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003911 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003912 if options.json_file:
3913 with open(options.json_file, 'wb') as f:
3914 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003915 return 0
3916
3917
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003918@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003919def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003920 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003921 parser.add_option('-d', '--display', action='store_true',
3922 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003923 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003924 help='New description to set for this issue (- for stdin, '
3925 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003926 parser.add_option('-f', '--force', action='store_true',
3927 help='Delete any unpublished Gerrit edits for this issue '
3928 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003929
3930 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003931 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003932 options, args = parser.parse_args(args)
3933 _process_codereview_select_options(parser, options)
3934
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003935 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003936 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003937 target_issue_arg = ParseIssueNumberArgument(args[0])
3938 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003939 parser.print_help()
3940 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003941
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003942 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003943
martiniss6eda05f2016-06-30 10:18:35 -07003944 kwargs = {
3945 'auth_config': auth_config,
3946 'codereview': options.forced_codereview,
3947 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003948 if target_issue_arg:
3949 kwargs['issue'] = target_issue_arg.issue
3950 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07003951
3952 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003953
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003954 if not cl.GetIssue():
3955 DieWithError('This branch has no associated changelist.')
3956 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003957
smut@google.com34fb6b12015-07-13 20:03:26 +00003958 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003959 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003960 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003961
3962 if options.new_description:
3963 text = options.new_description
3964 if text == '-':
3965 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003966 elif text == '+':
3967 base_branch = cl.GetCommonAncestorWithUpstream()
3968 change = cl.GetChange(base_branch, None, local_description=True)
3969 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003970
3971 description.set_description(text)
3972 else:
3973 description.prompt()
3974
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003975 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003976 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003977 return 0
3978
3979
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003980def CreateDescriptionFromLog(args):
3981 """Pulls out the commit log to use as a base for the CL description."""
3982 log_args = []
3983 if len(args) == 1 and not args[0].endswith('.'):
3984 log_args = [args[0] + '..']
3985 elif len(args) == 1 and args[0].endswith('...'):
3986 log_args = [args[0][:-1]]
3987 elif len(args) == 2:
3988 log_args = [args[0] + '..' + args[1]]
3989 else:
3990 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003991 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003992
3993
thestig@chromium.org44202a22014-03-11 19:22:18 +00003994def CMDlint(parser, args):
3995 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003996 parser.add_option('--filter', action='append', metavar='-x,+y',
3997 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003998 auth.add_auth_options(parser)
3999 options, args = parser.parse_args(args)
4000 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004001
4002 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004003 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004004 try:
4005 import cpplint
4006 import cpplint_chromium
4007 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004008 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004009 return 1
4010
4011 # Change the current working directory before calling lint so that it
4012 # shows the correct base.
4013 previous_cwd = os.getcwd()
4014 os.chdir(settings.GetRoot())
4015 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004016 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004017 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4018 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004019 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004020 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004021 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004022
4023 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004024 command = args + files
4025 if options.filter:
4026 command = ['--filter=' + ','.join(options.filter)] + command
4027 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004028
4029 white_regex = re.compile(settings.GetLintRegex())
4030 black_regex = re.compile(settings.GetLintIgnoreRegex())
4031 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4032 for filename in filenames:
4033 if white_regex.match(filename):
4034 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004035 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004036 else:
4037 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4038 extra_check_functions)
4039 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004040 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004041 finally:
4042 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004043 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004044 if cpplint._cpplint_state.error_count != 0:
4045 return 1
4046 return 0
4047
4048
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004049def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004050 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004051 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004052 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004053 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004054 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004055 auth.add_auth_options(parser)
4056 options, args = parser.parse_args(args)
4057 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004058
sbc@chromium.org71437c02015-04-09 19:29:40 +00004059 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004060 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004061 return 1
4062
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004063 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004064 if args:
4065 base_branch = args[0]
4066 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004067 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004068 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004069
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004070 cl.RunHook(
4071 committing=not options.upload,
4072 may_prompt=False,
4073 verbose=options.verbose,
4074 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004075 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004076
4077
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004078def GenerateGerritChangeId(message):
4079 """Returns Ixxxxxx...xxx change id.
4080
4081 Works the same way as
4082 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4083 but can be called on demand on all platforms.
4084
4085 The basic idea is to generate git hash of a state of the tree, original commit
4086 message, author/committer info and timestamps.
4087 """
4088 lines = []
4089 tree_hash = RunGitSilent(['write-tree'])
4090 lines.append('tree %s' % tree_hash.strip())
4091 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4092 if code == 0:
4093 lines.append('parent %s' % parent.strip())
4094 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4095 lines.append('author %s' % author.strip())
4096 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4097 lines.append('committer %s' % committer.strip())
4098 lines.append('')
4099 # Note: Gerrit's commit-hook actually cleans message of some lines and
4100 # whitespace. This code is not doing this, but it clearly won't decrease
4101 # entropy.
4102 lines.append(message)
4103 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4104 stdin='\n'.join(lines))
4105 return 'I%s' % change_hash.strip()
4106
4107
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004108def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004109 """Computes the remote branch ref to use for the CL.
4110
4111 Args:
4112 remote (str): The git remote for the CL.
4113 remote_branch (str): The git remote branch for the CL.
4114 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004115 """
4116 if not (remote and remote_branch):
4117 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004118
wittman@chromium.org455dc922015-01-26 20:15:50 +00004119 if target_branch:
4120 # Cannonicalize branch references to the equivalent local full symbolic
4121 # refs, which are then translated into the remote full symbolic refs
4122 # below.
4123 if '/' not in target_branch:
4124 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4125 else:
4126 prefix_replacements = (
4127 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4128 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4129 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4130 )
4131 match = None
4132 for regex, replacement in prefix_replacements:
4133 match = re.search(regex, target_branch)
4134 if match:
4135 remote_branch = target_branch.replace(match.group(0), replacement)
4136 break
4137 if not match:
4138 # This is a branch path but not one we recognize; use as-is.
4139 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004140 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4141 # Handle the refs that need to land in different refs.
4142 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004143
wittman@chromium.org455dc922015-01-26 20:15:50 +00004144 # Create the true path to the remote branch.
4145 # Does the following translation:
4146 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4147 # * refs/remotes/origin/master -> refs/heads/master
4148 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4149 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4150 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4151 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4152 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4153 'refs/heads/')
4154 elif remote_branch.startswith('refs/remotes/branch-heads'):
4155 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004156
wittman@chromium.org455dc922015-01-26 20:15:50 +00004157 return remote_branch
4158
4159
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004160def cleanup_list(l):
4161 """Fixes a list so that comma separated items are put as individual items.
4162
4163 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4164 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4165 """
4166 items = sum((i.split(',') for i in l), [])
4167 stripped_items = (i.strip() for i in items)
4168 return sorted(filter(None, stripped_items))
4169
4170
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004171@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004172def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004173 """Uploads the current changelist to codereview.
4174
4175 Can skip dependency patchset uploads for a branch by running:
4176 git config branch.branch_name.skip-deps-uploads True
4177 To unset run:
4178 git config --unset branch.branch_name.skip-deps-uploads
4179 Can also set the above globally by using the --global flag.
4180 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004181 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4182 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004183 parser.add_option('--bypass-watchlists', action='store_true',
4184 dest='bypass_watchlists',
4185 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004186 parser.add_option('-f', action='store_true', dest='force',
4187 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004188 parser.add_option('--message', '-m', dest='message',
4189 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004190 parser.add_option('-b', '--bug',
4191 help='pre-populate the bug number(s) for this issue. '
4192 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004193 parser.add_option('--message-file', dest='message_file',
4194 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004195 parser.add_option('--title', '-t', dest='title',
4196 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004197 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004198 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004199 help='reviewer email addresses')
4200 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004201 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004202 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004203 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004204 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004205 parser.add_option('--emulate_svn_auto_props',
4206 '--emulate-svn-auto-props',
4207 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004208 dest="emulate_svn_auto_props",
4209 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004210 parser.add_option('-c', '--use-commit-queue', action='store_true',
4211 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004212 parser.add_option('--private', action='store_true',
4213 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004214 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004215 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004216 metavar='TARGET',
4217 help='Apply CL to remote ref TARGET. ' +
4218 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004219 parser.add_option('--squash', action='store_true',
4220 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004221 parser.add_option('--no-squash', action='store_true',
4222 help='Don\'t squash multiple commits into one ' +
4223 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004224 parser.add_option('--topic', default=None,
4225 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004226 parser.add_option('--email', default=None,
4227 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004228 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4229 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004230 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4231 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004232 help='Send the patchset to do a CQ dry run right after '
4233 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004234 parser.add_option('--dependencies', action='store_true',
4235 help='Uploads CLs of all the local branches that depend on '
4236 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004237
rmistry@google.com2dd99862015-06-22 12:22:18 +00004238 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004239 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004240 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004241 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004242 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004243 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004244 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004245
sbc@chromium.org71437c02015-04-09 19:29:40 +00004246 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004247 return 1
4248
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004249 options.reviewers = cleanup_list(options.reviewers)
4250 options.cc = cleanup_list(options.cc)
4251
tandriib80458a2016-06-23 12:20:07 -07004252 if options.message_file:
4253 if options.message:
4254 parser.error('only one of --message and --message-file allowed.')
4255 options.message = gclient_utils.FileRead(options.message_file)
4256 options.message_file = None
4257
tandrii4d0545a2016-07-06 03:56:49 -07004258 if options.cq_dry_run and options.use_commit_queue:
4259 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4260
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004261 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4262 settings.GetIsGerrit()
4263
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004264 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004265 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004266
4267
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004268@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004269def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004270 """DEPRECATED: Used to commit the current changelist via git-svn."""
4271 message = ('git-cl no longer supports committing to SVN repositories via '
4272 'git-svn. You probably want to use `git cl land` instead.')
4273 print(message)
4274 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004275
4276
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004277@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004278def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004279 """Commits the current changelist via git.
4280
4281 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4282 upstream and closes the issue automatically and atomically.
4283
4284 Otherwise (in case of Rietveld):
4285 Squashes branch into a single commit.
4286 Updates commit message with metadata (e.g. pointer to review).
4287 Pushes the code upstream.
4288 Updates review and closes.
4289 """
4290 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4291 help='bypass upload presubmit hook')
4292 parser.add_option('-m', dest='message',
4293 help="override review description")
4294 parser.add_option('-f', action='store_true', dest='force',
4295 help="force yes to questions (don't prompt)")
4296 parser.add_option('-c', dest='contributor',
4297 help="external contributor for patch (appended to " +
4298 "description and used as author for git). Should be " +
4299 "formatted as 'First Last <email@example.com>'")
4300 add_git_similarity(parser)
4301 auth.add_auth_options(parser)
4302 (options, args) = parser.parse_args(args)
4303 auth_config = auth.extract_auth_config_from_options(options)
4304
4305 cl = Changelist(auth_config=auth_config)
4306
4307 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4308 if cl.IsGerrit():
4309 if options.message:
4310 # This could be implemented, but it requires sending a new patch to
4311 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4312 # Besides, Gerrit has the ability to change the commit message on submit
4313 # automatically, thus there is no need to support this option (so far?).
4314 parser.error('-m MESSAGE option is not supported for Gerrit.')
4315 if options.contributor:
4316 parser.error(
4317 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4318 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4319 'the contributor\'s "name <email>". If you can\'t upload such a '
4320 'commit for review, contact your repository admin and request'
4321 '"Forge-Author" permission.')
4322 if not cl.GetIssue():
4323 DieWithError('You must upload the change first to Gerrit.\n'
4324 ' If you would rather have `git cl land` upload '
4325 'automatically for you, see http://crbug.com/642759')
4326 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4327 options.verbose)
4328
4329 current = cl.GetBranch()
4330 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4331 if remote == '.':
4332 print()
4333 print('Attempting to push branch %r into another local branch!' % current)
4334 print()
4335 print('Either reparent this branch on top of origin/master:')
4336 print(' git reparent-branch --root')
4337 print()
4338 print('OR run `git rebase-update` if you think the parent branch is ')
4339 print('already committed.')
4340 print()
4341 print(' Current parent: %r' % upstream_branch)
4342 return 1
4343
4344 if not args:
4345 # Default to merging against our best guess of the upstream branch.
4346 args = [cl.GetUpstreamBranch()]
4347
4348 if options.contributor:
4349 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4350 print("Please provide contibutor as 'First Last <email@example.com>'")
4351 return 1
4352
4353 base_branch = args[0]
4354
4355 if git_common.is_dirty_git_tree('land'):
4356 return 1
4357
4358 # This rev-list syntax means "show all commits not in my branch that
4359 # are in base_branch".
4360 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4361 base_branch]).splitlines()
4362 if upstream_commits:
4363 print('Base branch "%s" has %d commits '
4364 'not in this branch.' % (base_branch, len(upstream_commits)))
4365 print('Run "git merge %s" before attempting to land.' % base_branch)
4366 return 1
4367
4368 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4369 if not options.bypass_hooks:
4370 author = None
4371 if options.contributor:
4372 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4373 hook_results = cl.RunHook(
4374 committing=True,
4375 may_prompt=not options.force,
4376 verbose=options.verbose,
4377 change=cl.GetChange(merge_base, author))
4378 if not hook_results.should_continue():
4379 return 1
4380
4381 # Check the tree status if the tree status URL is set.
4382 status = GetTreeStatus()
4383 if 'closed' == status:
4384 print('The tree is closed. Please wait for it to reopen. Use '
4385 '"git cl land --bypass-hooks" to commit on a closed tree.')
4386 return 1
4387 elif 'unknown' == status:
4388 print('Unable to determine tree status. Please verify manually and '
4389 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4390 return 1
4391
4392 change_desc = ChangeDescription(options.message)
4393 if not change_desc.description and cl.GetIssue():
4394 change_desc = ChangeDescription(cl.GetDescription())
4395
4396 if not change_desc.description:
4397 if not cl.GetIssue() and options.bypass_hooks:
4398 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4399 else:
4400 print('No description set.')
4401 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4402 return 1
4403
4404 # Keep a separate copy for the commit message, because the commit message
4405 # contains the link to the Rietveld issue, while the Rietveld message contains
4406 # the commit viewvc url.
4407 if cl.GetIssue():
4408 change_desc.update_reviewers(cl.GetApprovingReviewers())
4409
4410 commit_desc = ChangeDescription(change_desc.description)
4411 if cl.GetIssue():
4412 # Xcode won't linkify this URL unless there is a non-whitespace character
4413 # after it. Add a period on a new line to circumvent this. Also add a space
4414 # before the period to make sure that Gitiles continues to correctly resolve
4415 # the URL.
4416 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4417 if options.contributor:
4418 commit_desc.append_footer('Patch from %s.' % options.contributor)
4419
4420 print('Description:')
4421 print(commit_desc.description)
4422
4423 branches = [merge_base, cl.GetBranchRef()]
4424 if not options.force:
4425 print_stats(options.similarity, options.find_copies, branches)
4426
4427 # We want to squash all this branch's commits into one commit with the proper
4428 # description. We do this by doing a "reset --soft" to the base branch (which
4429 # keeps the working copy the same), then landing that.
4430 MERGE_BRANCH = 'git-cl-commit'
4431 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4432 # Delete the branches if they exist.
4433 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4434 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4435 result = RunGitWithCode(showref_cmd)
4436 if result[0] == 0:
4437 RunGit(['branch', '-D', branch])
4438
4439 # We might be in a directory that's present in this branch but not in the
4440 # trunk. Move up to the top of the tree so that git commands that expect a
4441 # valid CWD won't fail after we check out the merge branch.
4442 rel_base_path = settings.GetRelativeRoot()
4443 if rel_base_path:
4444 os.chdir(rel_base_path)
4445
4446 # Stuff our change into the merge branch.
4447 # We wrap in a try...finally block so if anything goes wrong,
4448 # we clean up the branches.
4449 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004450 revision = None
4451 try:
4452 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4453 RunGit(['reset', '--soft', merge_base])
4454 if options.contributor:
4455 RunGit(
4456 [
4457 'commit', '--author', options.contributor,
4458 '-m', commit_desc.description,
4459 ])
4460 else:
4461 RunGit(['commit', '-m', commit_desc.description])
4462
4463 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4464 mirror = settings.GetGitMirror(remote)
4465 if mirror:
4466 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004467 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004468 else:
4469 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004470 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004471 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4472
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004473 if git_numberer_enabled:
4474 # TODO(tandrii): maybe do autorebase + retry on failure
4475 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004476 logging.debug('Adding git number footers')
4477 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4478 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4479 branch)
4480 # Ensure timestamps are monotonically increasing.
4481 timestamp = max(1 + _get_committer_timestamp(merge_base),
4482 _get_committer_timestamp('HEAD'))
4483 _git_amend_head(commit_desc.description, timestamp)
4484 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004485
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004486 retcode, output = RunGitWithCode(
4487 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004488 if retcode == 0:
4489 revision = RunGit(['rev-parse', 'HEAD']).strip()
4490 logging.debug(output)
4491 except: # pylint: disable=bare-except
4492 if _IS_BEING_TESTED:
4493 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4494 + '-' * 30 + '8<' + '-' * 30)
4495 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4496 raise
4497 finally:
4498 # And then swap back to the original branch and clean up.
4499 RunGit(['checkout', '-q', cl.GetBranch()])
4500 RunGit(['branch', '-D', MERGE_BRANCH])
4501
4502 if not revision:
4503 print('Failed to push. If this persists, please file a bug.')
4504 return 1
4505
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004506 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004507 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004508 if viewvc_url and revision:
4509 change_desc.append_footer(
4510 'Committed: %s%s' % (viewvc_url, revision))
4511 elif revision:
4512 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004513 print('Closing issue '
4514 '(you may be prompted for your codereview password)...')
4515 cl.UpdateDescription(change_desc.description)
4516 cl.CloseIssue()
4517 props = cl.GetIssueProperties()
4518 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004519 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4520 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004521 if options.bypass_hooks:
4522 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4523 else:
4524 comment += ' (presubmit successful).'
4525 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4526
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004527 if os.path.isfile(POSTUPSTREAM_HOOK):
4528 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4529
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004530 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004531
4532
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004533@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004534def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004535 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004536 parser.add_option('-b', dest='newbranch',
4537 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004538 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004539 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004540 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4541 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004542 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004543 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004544 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004545 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004546 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004547 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004548
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004549
4550 group = optparse.OptionGroup(
4551 parser,
4552 'Options for continuing work on the current issue uploaded from a '
4553 'different clone (e.g. different machine). Must be used independently '
4554 'from the other options. No issue number should be specified, and the '
4555 'branch must have an issue number associated with it')
4556 group.add_option('--reapply', action='store_true', dest='reapply',
4557 help='Reset the branch and reapply the issue.\n'
4558 'CAUTION: This will undo any local changes in this '
4559 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004560
4561 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004562 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004563 parser.add_option_group(group)
4564
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004565 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004566 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004567 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004568 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004569 auth_config = auth.extract_auth_config_from_options(options)
4570
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004571
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004572 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004573 if options.newbranch:
4574 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004575 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004576 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004577
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004578 cl = Changelist(auth_config=auth_config,
4579 codereview=options.forced_codereview)
4580 if not cl.GetIssue():
4581 parser.error('current branch must have an associated issue')
4582
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004583 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004584 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004585 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004586
4587 RunGit(['reset', '--hard', upstream])
4588 if options.pull:
4589 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004590
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004591 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4592 options.directory)
4593
4594 if len(args) != 1 or not args[0]:
4595 parser.error('Must specify issue number or url')
4596
4597 # We don't want uncommitted changes mixed up with the patch.
4598 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004599 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004600
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004601 if options.newbranch:
4602 if options.force:
4603 RunGit(['branch', '-D', options.newbranch],
4604 stderr=subprocess2.PIPE, error_ok=True)
4605 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004606 elif not GetCurrentBranch():
4607 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004608
4609 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4610
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004611 if cl.IsGerrit():
4612 if options.reject:
4613 parser.error('--reject is not supported with Gerrit codereview.')
4614 if options.nocommit:
4615 parser.error('--nocommit is not supported with Gerrit codereview.')
4616 if options.directory:
4617 parser.error('--directory is not supported with Gerrit codereview.')
4618
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004619 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004620 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004621
4622
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004623def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004624 """Fetches the tree status and returns either 'open', 'closed',
4625 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004626 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004627 if url:
4628 status = urllib2.urlopen(url).read().lower()
4629 if status.find('closed') != -1 or status == '0':
4630 return 'closed'
4631 elif status.find('open') != -1 or status == '1':
4632 return 'open'
4633 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004634 return 'unset'
4635
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004636
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004637def GetTreeStatusReason():
4638 """Fetches the tree status from a json url and returns the message
4639 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004640 url = settings.GetTreeStatusUrl()
4641 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004642 connection = urllib2.urlopen(json_url)
4643 status = json.loads(connection.read())
4644 connection.close()
4645 return status['message']
4646
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004647
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004648def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004649 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004650 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004651 status = GetTreeStatus()
4652 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004653 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004654 return 2
4655
vapiera7fbd5a2016-06-16 09:17:49 -07004656 print('The tree is %s' % status)
4657 print()
4658 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004659 if status != 'open':
4660 return 1
4661 return 0
4662
4663
maruel@chromium.org15192402012-09-06 12:38:29 +00004664def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004665 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004666 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004667 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004668 '-b', '--bot', action='append',
4669 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4670 'times to specify multiple builders. ex: '
4671 '"-b win_rel -b win_layout". See '
4672 'the try server waterfall for the builders name and the tests '
4673 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004674 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004675 '-B', '--bucket', default='',
4676 help=('Buildbucket bucket to send the try requests.'))
4677 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004678 '-m', '--master', default='',
4679 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004680 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004681 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004682 help='Revision to use for the try job; default: the revision will '
4683 'be determined by the try recipe that builder runs, which usually '
4684 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004685 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004686 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004687 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004688 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004689 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004690 '--project',
4691 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004692 'in recipe to determine to which repository or directory to '
4693 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004694 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004695 '-p', '--property', dest='properties', action='append', default=[],
4696 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004697 'key2=value2 etc. The value will be treated as '
4698 'json if decodable, or as string otherwise. '
4699 'NOTE: using this may make your try job not usable for CQ, '
4700 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004701 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004702 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4703 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004704 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004705 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004706 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004707 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004708
machenbach@chromium.org45453142015-09-15 08:45:22 +00004709 # Make sure that all properties are prop=value pairs.
4710 bad_params = [x for x in options.properties if '=' not in x]
4711 if bad_params:
4712 parser.error('Got properties with missing "=": %s' % bad_params)
4713
maruel@chromium.org15192402012-09-06 12:38:29 +00004714 if args:
4715 parser.error('Unknown arguments: %s' % args)
4716
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004717 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004718 if not cl.GetIssue():
4719 parser.error('Need to upload first')
4720
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004721 if cl.IsGerrit():
4722 # HACK: warm up Gerrit change detail cache to save on RPCs.
4723 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4724
tandriie113dfd2016-10-11 10:20:12 -07004725 error_message = cl.CannotTriggerTryJobReason()
4726 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004727 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004728
borenet6c0efe62016-10-19 08:13:29 -07004729 if options.bucket and options.master:
4730 parser.error('Only one of --bucket and --master may be used.')
4731
qyearsley1fdfcb62016-10-24 13:22:03 -07004732 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004733
qyearsleydd49f942016-10-28 11:57:22 -07004734 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4735 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004736 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004737 if options.verbose:
4738 print('git cl try with no bots now defaults to CQ Dry Run.')
4739 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004740
borenet6c0efe62016-10-19 08:13:29 -07004741 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004742 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004743 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004744 'of bot requires an initial job from a parent (usually a builder). '
4745 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004746 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004747 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004748
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004749 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004750 # TODO(tandrii): Checking local patchset against remote patchset is only
4751 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4752 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004753 print('Warning: Codereview server has newer patchsets (%s) than most '
4754 'recent upload from local checkout (%s). Did a previous upload '
4755 'fail?\n'
4756 'By default, git cl try uses the latest patchset from '
4757 'codereview, continuing to use patchset %s.\n' %
4758 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004759
tandrii568043b2016-10-11 07:49:18 -07004760 try:
borenet6c0efe62016-10-19 08:13:29 -07004761 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4762 patchset)
tandrii568043b2016-10-11 07:49:18 -07004763 except BuildbucketResponseException as ex:
4764 print('ERROR: %s' % ex)
4765 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004766 return 0
4767
4768
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004769def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004770 """Prints info about try jobs associated with current CL."""
4771 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004772 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004773 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004774 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004775 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004776 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004777 '--color', action='store_true', default=setup_color.IS_TTY,
4778 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004779 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004780 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4781 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004782 group.add_option(
4783 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004784 parser.add_option_group(group)
4785 auth.add_auth_options(parser)
4786 options, args = parser.parse_args(args)
4787 if args:
4788 parser.error('Unrecognized args: %s' % ' '.join(args))
4789
4790 auth_config = auth.extract_auth_config_from_options(options)
4791 cl = Changelist(auth_config=auth_config)
4792 if not cl.GetIssue():
4793 parser.error('Need to upload first')
4794
tandrii221ab252016-10-06 08:12:04 -07004795 patchset = options.patchset
4796 if not patchset:
4797 patchset = cl.GetMostRecentPatchset()
4798 if not patchset:
4799 parser.error('Codereview doesn\'t know about issue %s. '
4800 'No access to issue or wrong issue number?\n'
4801 'Either upload first, or pass --patchset explicitely' %
4802 cl.GetIssue())
4803
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004804 # TODO(tandrii): Checking local patchset against remote patchset is only
4805 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4806 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004807 print('Warning: Codereview server has newer patchsets (%s) than most '
4808 'recent upload from local checkout (%s). Did a previous upload '
4809 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004810 'By default, git cl try-results uses the latest patchset from '
4811 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004812 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004813 try:
tandrii221ab252016-10-06 08:12:04 -07004814 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004815 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004816 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004817 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004818 if options.json:
4819 write_try_results_json(options.json, jobs)
4820 else:
4821 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004822 return 0
4823
4824
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004825@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004826def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004827 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004828 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004829 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004830 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004832 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004833 if args:
4834 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004835 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004836 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004837 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004838 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004839
4840 # Clear configured merge-base, if there is one.
4841 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004842 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004843 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004844 return 0
4845
4846
thestig@chromium.org00858c82013-12-02 23:08:03 +00004847def CMDweb(parser, args):
4848 """Opens the current CL in the web browser."""
4849 _, args = parser.parse_args(args)
4850 if args:
4851 parser.error('Unrecognized args: %s' % ' '.join(args))
4852
4853 issue_url = Changelist().GetIssueURL()
4854 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004855 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004856 return 1
4857
4858 webbrowser.open(issue_url)
4859 return 0
4860
4861
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004862def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004863 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004864 parser.add_option('-d', '--dry-run', action='store_true',
4865 help='trigger in dry run mode')
4866 parser.add_option('-c', '--clear', action='store_true',
4867 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004868 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004869 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004870 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004871 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004872 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004873 if args:
4874 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004875 if options.dry_run and options.clear:
4876 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4877
iannuccie53c9352016-08-17 14:40:40 -07004878 cl = Changelist(auth_config=auth_config, issue=options.issue,
4879 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004880 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004881 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004882 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004883 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004884 state = _CQState.DRY_RUN
4885 else:
4886 state = _CQState.COMMIT
4887 if not cl.GetIssue():
4888 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004889 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004890 return 0
4891
4892
groby@chromium.org411034a2013-02-26 15:12:01 +00004893def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004894 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004895 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004896 auth.add_auth_options(parser)
4897 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004898 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004899 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004900 if args:
4901 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004902 cl = Changelist(auth_config=auth_config, issue=options.issue,
4903 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004904 # Ensure there actually is an issue to close.
4905 cl.GetDescription()
4906 cl.CloseIssue()
4907 return 0
4908
4909
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004910def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004911 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004912 parser.add_option(
4913 '--stat',
4914 action='store_true',
4915 dest='stat',
4916 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004917 auth.add_auth_options(parser)
4918 options, args = parser.parse_args(args)
4919 auth_config = auth.extract_auth_config_from_options(options)
4920 if args:
4921 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004922
4923 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004924 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004925 # Staged changes would be committed along with the patch from last
4926 # upload, hence counted toward the "last upload" side in the final
4927 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004928 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004929 return 1
4930
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004931 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004932 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004933 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004934 if not issue:
4935 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004936 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004937 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004938
4939 # Create a new branch based on the merge-base
4940 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004941 # Clear cached branch in cl object, to avoid overwriting original CL branch
4942 # properties.
4943 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004944 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004945 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004946 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004947 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004948 return rtn
4949
wychen@chromium.org06928532015-02-03 02:11:29 +00004950 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004951 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004952 cmd = ['git', 'diff']
4953 if options.stat:
4954 cmd.append('--stat')
4955 cmd.extend([TMP_BRANCH, branch, '--'])
4956 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004957 finally:
4958 RunGit(['checkout', '-q', branch])
4959 RunGit(['branch', '-D', TMP_BRANCH])
4960
4961 return 0
4962
4963
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004964def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004965 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004966 parser.add_option(
4967 '--no-color',
4968 action='store_true',
4969 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004970 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004971 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004972 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004973
4974 author = RunGit(['config', 'user.email']).strip() or None
4975
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004976 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004977
4978 if args:
4979 if len(args) > 1:
4980 parser.error('Unknown args')
4981 base_branch = args[0]
4982 else:
4983 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004984 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004985
4986 change = cl.GetChange(base_branch, None)
4987 return owners_finder.OwnersFinder(
4988 [f.LocalPath() for f in
4989 cl.GetChange(base_branch, None).AffectedFiles()],
4990 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004991 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004992 disable_color=options.no_color).run()
4993
4994
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004995def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004996 """Generates a diff command."""
4997 # Generate diff for the current branch's changes.
4998 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004999 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005000
5001 if args:
5002 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005003 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005004 diff_cmd.append(arg)
5005 else:
5006 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005007
5008 return diff_cmd
5009
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005010
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005011def MatchingFileType(file_name, extensions):
5012 """Returns true if the file name ends with one of the given extensions."""
5013 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005014
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005015
enne@chromium.org555cfe42014-01-29 18:21:39 +00005016@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005017def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005018 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005019 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005020 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005021 parser.add_option('--full', action='store_true',
5022 help='Reformat the full content of all touched files')
5023 parser.add_option('--dry-run', action='store_true',
5024 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005025 parser.add_option('--python', action='store_true',
5026 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005027 parser.add_option('--js', action='store_true',
5028 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005029 parser.add_option('--diff', action='store_true',
5030 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005031 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005032
Daniel Chengc55eecf2016-12-30 03:11:02 -08005033 # Normalize any remaining args against the current path, so paths relative to
5034 # the current directory are still resolved as expected.
5035 args = [os.path.join(os.getcwd(), arg) for arg in args]
5036
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005037 # git diff generates paths against the root of the repository. Change
5038 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005039 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005040 if rel_base_path:
5041 os.chdir(rel_base_path)
5042
digit@chromium.org29e47272013-05-17 17:01:46 +00005043 # Grab the merge-base commit, i.e. the upstream commit of the current
5044 # branch when it was created or the last time it was rebased. This is
5045 # to cover the case where the user may have called "git fetch origin",
5046 # moving the origin branch to a newer commit, but hasn't rebased yet.
5047 upstream_commit = None
5048 cl = Changelist()
5049 upstream_branch = cl.GetUpstreamBranch()
5050 if upstream_branch:
5051 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5052 upstream_commit = upstream_commit.strip()
5053
5054 if not upstream_commit:
5055 DieWithError('Could not find base commit for this branch. '
5056 'Are you in detached state?')
5057
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005058 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5059 diff_output = RunGit(changed_files_cmd)
5060 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005061 # Filter out files deleted by this CL
5062 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005063
Christopher Lamc5ba6922017-01-24 11:19:14 +11005064 if opts.js:
5065 CLANG_EXTS.append('.js')
5066
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005067 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5068 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5069 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005070 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005071
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005072 top_dir = os.path.normpath(
5073 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5074
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005075 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5076 # formatted. This is used to block during the presubmit.
5077 return_value = 0
5078
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005079 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005080 # Locate the clang-format binary in the checkout
5081 try:
5082 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005083 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005084 DieWithError(e)
5085
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005086 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005087 cmd = [clang_format_tool]
5088 if not opts.dry_run and not opts.diff:
5089 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005090 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005091 if opts.diff:
5092 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005093 else:
5094 env = os.environ.copy()
5095 env['PATH'] = str(os.path.dirname(clang_format_tool))
5096 try:
5097 script = clang_format.FindClangFormatScriptInChromiumTree(
5098 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005099 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005100 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005101
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005102 cmd = [sys.executable, script, '-p0']
5103 if not opts.dry_run and not opts.diff:
5104 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005105
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005106 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5107 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005108
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005109 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5110 if opts.diff:
5111 sys.stdout.write(stdout)
5112 if opts.dry_run and len(stdout) > 0:
5113 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005114
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005115 # Similar code to above, but using yapf on .py files rather than clang-format
5116 # on C/C++ files
5117 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005118 yapf_tool = gclient_utils.FindExecutable('yapf')
5119 if yapf_tool is None:
5120 DieWithError('yapf not found in PATH')
5121
5122 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005123 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005124 cmd = [yapf_tool]
5125 if not opts.dry_run and not opts.diff:
5126 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005127 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005128 if opts.diff:
5129 sys.stdout.write(stdout)
5130 else:
5131 # TODO(sbc): yapf --lines mode still has some issues.
5132 # https://github.com/google/yapf/issues/154
5133 DieWithError('--python currently only works with --full')
5134
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005135 # Dart's formatter does not have the nice property of only operating on
5136 # modified chunks, so hard code full.
5137 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005138 try:
5139 command = [dart_format.FindDartFmtToolInChromiumTree()]
5140 if not opts.dry_run and not opts.diff:
5141 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005142 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005143
ppi@chromium.org6593d932016-03-03 15:41:15 +00005144 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005145 if opts.dry_run and stdout:
5146 return_value = 2
5147 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005148 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5149 'found in this checkout. Files in other languages are still '
5150 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005151
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005152 # Format GN build files. Always run on full build files for canonical form.
5153 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005154 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005155 if opts.dry_run or opts.diff:
5156 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005157 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005158 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5159 shell=sys.platform == 'win32',
5160 cwd=top_dir)
5161 if opts.dry_run and gn_ret == 2:
5162 return_value = 2 # Not formatted.
5163 elif opts.diff and gn_ret == 2:
5164 # TODO this should compute and print the actual diff.
5165 print("This change has GN build file diff for " + gn_diff_file)
5166 elif gn_ret != 0:
5167 # For non-dry run cases (and non-2 return values for dry-run), a
5168 # nonzero error code indicates a failure, probably because the file
5169 # doesn't parse.
5170 DieWithError("gn format failed on " + gn_diff_file +
5171 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005172
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005173 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005174
5175
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005176@subcommand.usage('<codereview url or issue id>')
5177def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005178 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005179 _, args = parser.parse_args(args)
5180
5181 if len(args) != 1:
5182 parser.print_help()
5183 return 1
5184
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005185 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005186 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005187 parser.print_help()
5188 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005189 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005190
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005191 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005192 output = RunGit(['config', '--local', '--get-regexp',
5193 r'branch\..*\.%s' % issueprefix],
5194 error_ok=True)
5195 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005196 if issue == target_issue:
5197 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005198
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005199 branches = []
5200 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005201 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005202 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005203 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005204 return 1
5205 if len(branches) == 1:
5206 RunGit(['checkout', branches[0]])
5207 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005208 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005209 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005210 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005211 which = raw_input('Choose by index: ')
5212 try:
5213 RunGit(['checkout', branches[int(which)]])
5214 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005215 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005216 return 1
5217
5218 return 0
5219
5220
maruel@chromium.org29404b52014-09-08 22:58:00 +00005221def CMDlol(parser, args):
5222 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005223 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005224 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5225 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5226 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005227 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005228 return 0
5229
5230
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005231class OptionParser(optparse.OptionParser):
5232 """Creates the option parse and add --verbose support."""
5233 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005234 optparse.OptionParser.__init__(
5235 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005236 self.add_option(
5237 '-v', '--verbose', action='count', default=0,
5238 help='Use 2 times for more debugging info')
5239
5240 def parse_args(self, args=None, values=None):
5241 options, args = optparse.OptionParser.parse_args(self, args, values)
5242 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005243 logging.basicConfig(
5244 level=levels[min(options.verbose, len(levels) - 1)],
5245 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5246 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005247 return options, args
5248
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005249
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005250def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005251 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005252 print('\nYour python version %s is unsupported, please upgrade.\n' %
5253 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005254 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005255
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005256 # Reload settings.
5257 global settings
5258 settings = Settings()
5259
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005260 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005261 dispatcher = subcommand.CommandDispatcher(__name__)
5262 try:
5263 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005264 except auth.AuthenticationError as e:
5265 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005266 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005267 if e.code != 500:
5268 raise
5269 DieWithError(
5270 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5271 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005272 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005273
5274
5275if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005276 # These affect sys.stdout so do it outside of main() to simplify mocks in
5277 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005278 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005279 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005280 try:
5281 sys.exit(main(sys.argv[1:]))
5282 except KeyboardInterrupt:
5283 sys.stderr.write('interrupted\n')
5284 sys.exit(1)