blob: 8bf9ff2b9d2b166bf462b58e6a0f7875fcb37b84 [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:
Andrii Shyshkalovaf3a9992017-02-09 14:18:01 +01002802 # TODO(tandrii): remove this horrible hack once (Poly)Gerrit fixes their
2803 # side for real (b/34702620).
2804 def clean_invisible_chars(email):
2805 return email.decode('unicode_escape').encode('ascii', 'ignore')
2806 refspec_opts.extend('r=' + clean_invisible_chars(email).strip()
2807 for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002808
agablec6787972016-09-09 16:13:34 -07002809 if options.private:
2810 refspec_opts.append('draft')
2811
rmistry9eadede2016-09-19 11:22:43 -07002812 if options.topic:
2813 # Documentation on Gerrit topics is here:
2814 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2815 refspec_opts.append('topic=%s' % options.topic)
2816
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002817 refspec_suffix = ''
2818 if refspec_opts:
2819 refspec_suffix = '%' + ','.join(refspec_opts)
2820 assert ' ' not in refspec_suffix, (
2821 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002822 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002823
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002824 try:
2825 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalove9c78ff2017-02-06 15:53:13 +01002826 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002827 print_stdout=True,
2828 # Flush after every line: useful for seeing progress when running as
2829 # recipe.
2830 filter_fn=lambda _: sys.stdout.flush())
2831 except subprocess2.CalledProcessError:
2832 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002833 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002834
2835 if options.squash:
2836 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2837 change_numbers = [m.group(1)
2838 for m in map(regex.match, push_stdout.splitlines())
2839 if m]
2840 if len(change_numbers) != 1:
2841 DieWithError(
2842 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002843 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002844 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002845 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002846
2847 # Add cc's from the CC_LIST and --cc flag (if any).
2848 cc = self.GetCCList().split(',')
2849 if options.cc:
2850 cc.extend(options.cc)
2851 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002852 if change_desc.get_cced():
2853 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002854 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002855 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002856 self._GetGerritHost(), self.GetIssue(), cc,
2857 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002858 return 0
2859
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002860 def _AddChangeIdToCommitMessage(self, options, args):
2861 """Re-commits using the current message, assumes the commit hook is in
2862 place.
2863 """
2864 log_desc = options.message or CreateDescriptionFromLog(args)
2865 git_command = ['commit', '--amend', '-m', log_desc]
2866 RunGit(git_command)
2867 new_log_desc = CreateDescriptionFromLog(args)
2868 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002869 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002870 return new_log_desc
2871 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002872 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002873
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002874 def SetCQState(self, new_state):
2875 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002876 vote_map = {
2877 _CQState.NONE: 0,
2878 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002879 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002880 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002881 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2882 if new_state == _CQState.DRY_RUN:
2883 # Don't spam everybody reviewer/owner.
2884 kwargs['notify'] = 'NONE'
2885 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002886
tandriie113dfd2016-10-11 10:20:12 -07002887 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002888 try:
2889 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002890 except GerritChangeNotExists:
2891 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002892
2893 if data['status'] in ('ABANDONED', 'MERGED'):
2894 return 'CL %s is closed' % self.GetIssue()
2895
2896 def GetTryjobProperties(self, patchset=None):
2897 """Returns dictionary of properties to launch tryjob."""
2898 data = self._GetChangeDetail(['ALL_REVISIONS'])
2899 patchset = int(patchset or self.GetPatchset())
2900 assert patchset
2901 revision_data = None # Pylint wants it to be defined.
2902 for revision_data in data['revisions'].itervalues():
2903 if int(revision_data['_number']) == patchset:
2904 break
2905 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002906 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002907 (patchset, self.GetIssue()))
2908 return {
2909 'patch_issue': self.GetIssue(),
2910 'patch_set': patchset or self.GetPatchset(),
2911 'patch_project': data['project'],
2912 'patch_storage': 'gerrit',
2913 'patch_ref': revision_data['fetch']['http']['ref'],
2914 'patch_repository_url': revision_data['fetch']['http']['url'],
2915 'patch_gerrit_url': self.GetCodereviewServer(),
2916 }
tandriie113dfd2016-10-11 10:20:12 -07002917
tandriide281ae2016-10-12 06:02:30 -07002918 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002919 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002920
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002921
2922_CODEREVIEW_IMPLEMENTATIONS = {
2923 'rietveld': _RietveldChangelistImpl,
2924 'gerrit': _GerritChangelistImpl,
2925}
2926
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002927
iannuccie53c9352016-08-17 14:40:40 -07002928def _add_codereview_issue_select_options(parser, extra=""):
2929 _add_codereview_select_options(parser)
2930
2931 text = ('Operate on this issue number instead of the current branch\'s '
2932 'implicit issue.')
2933 if extra:
2934 text += ' '+extra
2935 parser.add_option('-i', '--issue', type=int, help=text)
2936
2937
2938def _process_codereview_issue_select_options(parser, options):
2939 _process_codereview_select_options(parser, options)
2940 if options.issue is not None and not options.forced_codereview:
2941 parser.error('--issue must be specified with either --rietveld or --gerrit')
2942
2943
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002944def _add_codereview_select_options(parser):
2945 """Appends --gerrit and --rietveld options to force specific codereview."""
2946 parser.codereview_group = optparse.OptionGroup(
2947 parser, 'EXPERIMENTAL! Codereview override options')
2948 parser.add_option_group(parser.codereview_group)
2949 parser.codereview_group.add_option(
2950 '--gerrit', action='store_true',
2951 help='Force the use of Gerrit for codereview')
2952 parser.codereview_group.add_option(
2953 '--rietveld', action='store_true',
2954 help='Force the use of Rietveld for codereview')
2955
2956
2957def _process_codereview_select_options(parser, options):
2958 if options.gerrit and options.rietveld:
2959 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2960 options.forced_codereview = None
2961 if options.gerrit:
2962 options.forced_codereview = 'gerrit'
2963 elif options.rietveld:
2964 options.forced_codereview = 'rietveld'
2965
2966
tandriif9aefb72016-07-01 09:06:51 -07002967def _get_bug_line_values(default_project, bugs):
2968 """Given default_project and comma separated list of bugs, yields bug line
2969 values.
2970
2971 Each bug can be either:
2972 * a number, which is combined with default_project
2973 * string, which is left as is.
2974
2975 This function may produce more than one line, because bugdroid expects one
2976 project per line.
2977
2978 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2979 ['v8:123', 'chromium:789']
2980 """
2981 default_bugs = []
2982 others = []
2983 for bug in bugs.split(','):
2984 bug = bug.strip()
2985 if bug:
2986 try:
2987 default_bugs.append(int(bug))
2988 except ValueError:
2989 others.append(bug)
2990
2991 if default_bugs:
2992 default_bugs = ','.join(map(str, default_bugs))
2993 if default_project:
2994 yield '%s:%s' % (default_project, default_bugs)
2995 else:
2996 yield default_bugs
2997 for other in sorted(others):
2998 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2999 yield other
3000
3001
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003002class ChangeDescription(object):
3003 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003004 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003005 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003006 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003007 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003008
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003009 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003010 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003011
agable@chromium.org42c20792013-09-12 17:34:49 +00003012 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003013 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003014 return '\n'.join(self._description_lines)
3015
3016 def set_description(self, desc):
3017 if isinstance(desc, basestring):
3018 lines = desc.splitlines()
3019 else:
3020 lines = [line.rstrip() for line in desc]
3021 while lines and not lines[0]:
3022 lines.pop(0)
3023 while lines and not lines[-1]:
3024 lines.pop(-1)
3025 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003026
piman@chromium.org336f9122014-09-04 02:16:55 +00003027 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003028 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003029 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003030 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003031 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003032 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003033
agable@chromium.org42c20792013-09-12 17:34:49 +00003034 # Get the set of R= and TBR= lines and remove them from the desciption.
3035 regexp = re.compile(self.R_LINE)
3036 matches = [regexp.match(line) for line in self._description_lines]
3037 new_desc = [l for i, l in enumerate(self._description_lines)
3038 if not matches[i]]
3039 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003040
agable@chromium.org42c20792013-09-12 17:34:49 +00003041 # Construct new unified R= and TBR= lines.
3042 r_names = []
3043 tbr_names = []
3044 for match in matches:
3045 if not match:
3046 continue
3047 people = cleanup_list([match.group(2).strip()])
3048 if match.group(1) == 'TBR':
3049 tbr_names.extend(people)
3050 else:
3051 r_names.extend(people)
3052 for name in r_names:
3053 if name not in reviewers:
3054 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003055 if add_owners_tbr:
3056 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003057 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003058 all_reviewers = set(tbr_names + reviewers)
3059 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3060 all_reviewers)
3061 tbr_names.extend(owners_db.reviewers_for(missing_files,
3062 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003063 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3064 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3065
3066 # Put the new lines in the description where the old first R= line was.
3067 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3068 if 0 <= line_loc < len(self._description_lines):
3069 if new_tbr_line:
3070 self._description_lines.insert(line_loc, new_tbr_line)
3071 if new_r_line:
3072 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003073 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003074 if new_r_line:
3075 self.append_footer(new_r_line)
3076 if new_tbr_line:
3077 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003078
tandriif9aefb72016-07-01 09:06:51 -07003079 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003080 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003081 self.set_description([
3082 '# Enter a description of the change.',
3083 '# This will be displayed on the codereview site.',
3084 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003085 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003086 '--------------------',
3087 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003088
agable@chromium.org42c20792013-09-12 17:34:49 +00003089 regexp = re.compile(self.BUG_LINE)
3090 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003091 prefix = settings.GetBugPrefix()
3092 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3093 for value in values:
3094 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3095 self.append_footer('BUG=%s' % value)
3096
agable@chromium.org42c20792013-09-12 17:34:49 +00003097 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003098 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003099 if not content:
3100 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003101 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003102
3103 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003104 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3105 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003106 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003107 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003108
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003109 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003110 """Adds a footer line to the description.
3111
3112 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3113 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3114 that Gerrit footers are always at the end.
3115 """
3116 parsed_footer_line = git_footers.parse_footer(line)
3117 if parsed_footer_line:
3118 # Line is a gerrit footer in the form: Footer-Key: any value.
3119 # Thus, must be appended observing Gerrit footer rules.
3120 self.set_description(
3121 git_footers.add_footer(self.description,
3122 key=parsed_footer_line[0],
3123 value=parsed_footer_line[1]))
3124 return
3125
3126 if not self._description_lines:
3127 self._description_lines.append(line)
3128 return
3129
3130 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3131 if gerrit_footers:
3132 # git_footers.split_footers ensures that there is an empty line before
3133 # actual (gerrit) footers, if any. We have to keep it that way.
3134 assert top_lines and top_lines[-1] == ''
3135 top_lines, separator = top_lines[:-1], top_lines[-1:]
3136 else:
3137 separator = [] # No need for separator if there are no gerrit_footers.
3138
3139 prev_line = top_lines[-1] if top_lines else ''
3140 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3141 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3142 top_lines.append('')
3143 top_lines.append(line)
3144 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003145
tandrii99a72f22016-08-17 14:33:24 -07003146 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003147 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003148 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003149 reviewers = [match.group(2).strip()
3150 for match in matches
3151 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003152 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003153
bradnelsond975b302016-10-23 12:20:23 -07003154 def get_cced(self):
3155 """Retrieves the list of reviewers."""
3156 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3157 cced = [match.group(2).strip() for match in matches if match]
3158 return cleanup_list(cced)
3159
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003160 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3161 """Updates this commit description given the parent.
3162
3163 This is essentially what Gnumbd used to do.
3164 Consult https://goo.gl/WMmpDe for more details.
3165 """
3166 assert parent_msg # No, orphan branch creation isn't supported.
3167 assert parent_hash
3168 assert dest_ref
3169 parent_footer_map = git_footers.parse_footers(parent_msg)
3170 # This will also happily parse svn-position, which GnumbD is no longer
3171 # supporting. While we'd generate correct footers, the verifier plugin
3172 # installed in Gerrit will block such commit (ie git push below will fail).
3173 parent_position = git_footers.get_position(parent_footer_map)
3174
3175 # Cherry-picks may have last line obscuring their prior footers,
3176 # from git_footers perspective. This is also what Gnumbd did.
3177 cp_line = None
3178 if (self._description_lines and
3179 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3180 cp_line = self._description_lines.pop()
3181
3182 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3183
3184 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3185 # user interference with actual footers we'd insert below.
3186 for i, (k, v) in enumerate(parsed_footers):
3187 if k.startswith('Cr-'):
3188 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3189
3190 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003191 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003192 if parent_position[0] == dest_ref:
3193 # Same branch as parent.
3194 number = int(parent_position[1]) + 1
3195 else:
3196 number = 1 # New branch, and extra lineage.
3197 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3198 int(parent_position[1])))
3199
3200 parsed_footers.append(('Cr-Commit-Position',
3201 '%s@{#%d}' % (dest_ref, number)))
3202 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3203
3204 self._description_lines = top_lines
3205 if cp_line:
3206 self._description_lines.append(cp_line)
3207 if self._description_lines[-1] != '':
3208 self._description_lines.append('') # Ensure footer separator.
3209 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3210
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003211
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003212def get_approving_reviewers(props):
3213 """Retrieves the reviewers that approved a CL from the issue properties with
3214 messages.
3215
3216 Note that the list may contain reviewers that are not committer, thus are not
3217 considered by the CQ.
3218 """
3219 return sorted(
3220 set(
3221 message['sender']
3222 for message in props['messages']
3223 if message['approval'] and message['sender'] in props['reviewers']
3224 )
3225 )
3226
3227
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003228def FindCodereviewSettingsFile(filename='codereview.settings'):
3229 """Finds the given file starting in the cwd and going up.
3230
3231 Only looks up to the top of the repository unless an
3232 'inherit-review-settings-ok' file exists in the root of the repository.
3233 """
3234 inherit_ok_file = 'inherit-review-settings-ok'
3235 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003236 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003237 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3238 root = '/'
3239 while True:
3240 if filename in os.listdir(cwd):
3241 if os.path.isfile(os.path.join(cwd, filename)):
3242 return open(os.path.join(cwd, filename))
3243 if cwd == root:
3244 break
3245 cwd = os.path.dirname(cwd)
3246
3247
3248def LoadCodereviewSettingsFromFile(fileobj):
3249 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003250 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003251
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003252 def SetProperty(name, setting, unset_error_ok=False):
3253 fullname = 'rietveld.' + name
3254 if setting in keyvals:
3255 RunGit(['config', fullname, keyvals[setting]])
3256 else:
3257 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3258
tandrii48df5812016-10-17 03:55:37 -07003259 if not keyvals.get('GERRIT_HOST', False):
3260 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003261 # Only server setting is required. Other settings can be absent.
3262 # In that case, we ignore errors raised during option deletion attempt.
3263 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003264 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003265 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3266 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003267 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003268 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3269 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003270 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003271 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3272 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003273
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003274 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003275 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003276
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003277 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003278 RunGit(['config', 'gerrit.squash-uploads',
3279 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003280
tandrii@chromium.org28253532016-04-14 13:46:56 +00003281 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003282 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003283 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3284
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003285 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003286 # should be of the form
3287 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3288 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003289 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3290 keyvals['ORIGIN_URL_CONFIG']])
3291
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003292
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003293def urlretrieve(source, destination):
3294 """urllib is broken for SSL connections via a proxy therefore we
3295 can't use urllib.urlretrieve()."""
3296 with open(destination, 'w') as f:
3297 f.write(urllib2.urlopen(source).read())
3298
3299
ukai@chromium.org712d6102013-11-27 00:52:58 +00003300def hasSheBang(fname):
3301 """Checks fname is a #! script."""
3302 with open(fname) as f:
3303 return f.read(2).startswith('#!')
3304
3305
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003306# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3307def DownloadHooks(*args, **kwargs):
3308 pass
3309
3310
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003311def DownloadGerritHook(force):
3312 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003313
3314 Args:
3315 force: True to update hooks. False to install hooks if not present.
3316 """
3317 if not settings.GetIsGerrit():
3318 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003319 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003320 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3321 if not os.access(dst, os.X_OK):
3322 if os.path.exists(dst):
3323 if not force:
3324 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003325 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003326 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003327 if not hasSheBang(dst):
3328 DieWithError('Not a script: %s\n'
3329 'You need to download from\n%s\n'
3330 'into .git/hooks/commit-msg and '
3331 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003332 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3333 except Exception:
3334 if os.path.exists(dst):
3335 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003336 DieWithError('\nFailed to download hooks.\n'
3337 'You need to download from\n%s\n'
3338 'into .git/hooks/commit-msg and '
3339 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003340
3341
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003342def GetRietveldCodereviewSettingsInteractively():
3343 """Prompt the user for settings."""
3344 server = settings.GetDefaultServerUrl(error_ok=True)
3345 prompt = 'Rietveld server (host[:port])'
3346 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3347 newserver = ask_for_data(prompt + ':')
3348 if not server and not newserver:
3349 newserver = DEFAULT_SERVER
3350 if newserver:
3351 newserver = gclient_utils.UpgradeToHttps(newserver)
3352 if newserver != server:
3353 RunGit(['config', 'rietveld.server', newserver])
3354
3355 def SetProperty(initial, caption, name, is_url):
3356 prompt = caption
3357 if initial:
3358 prompt += ' ("x" to clear) [%s]' % initial
3359 new_val = ask_for_data(prompt + ':')
3360 if new_val == 'x':
3361 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3362 elif new_val:
3363 if is_url:
3364 new_val = gclient_utils.UpgradeToHttps(new_val)
3365 if new_val != initial:
3366 RunGit(['config', 'rietveld.' + name, new_val])
3367
3368 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3369 SetProperty(settings.GetDefaultPrivateFlag(),
3370 'Private flag (rietveld only)', 'private', False)
3371 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3372 'tree-status-url', False)
3373 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3374 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3375 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3376 'run-post-upload-hook', False)
3377
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003378
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003379@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003380def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003381 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003382
tandrii5d0a0422016-09-14 06:24:35 -07003383 print('WARNING: git cl config works for Rietveld only')
3384 # TODO(tandrii): remove this once we switch to Gerrit.
3385 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003386 parser.add_option('--activate-update', action='store_true',
3387 help='activate auto-updating [rietveld] section in '
3388 '.git/config')
3389 parser.add_option('--deactivate-update', action='store_true',
3390 help='deactivate auto-updating [rietveld] section in '
3391 '.git/config')
3392 options, args = parser.parse_args(args)
3393
3394 if options.deactivate_update:
3395 RunGit(['config', 'rietveld.autoupdate', 'false'])
3396 return
3397
3398 if options.activate_update:
3399 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3400 return
3401
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003402 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003403 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003404 return 0
3405
3406 url = args[0]
3407 if not url.endswith('codereview.settings'):
3408 url = os.path.join(url, 'codereview.settings')
3409
3410 # Load code review settings and download hooks (if available).
3411 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3412 return 0
3413
3414
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003415def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003416 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003417 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3418 branch = ShortBranchName(branchref)
3419 _, args = parser.parse_args(args)
3420 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003421 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003422 return RunGit(['config', 'branch.%s.base-url' % branch],
3423 error_ok=False).strip()
3424 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003425 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003426 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3427 error_ok=False).strip()
3428
3429
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003430def color_for_status(status):
3431 """Maps a Changelist status to color, for CMDstatus and other tools."""
3432 return {
3433 'unsent': Fore.RED,
3434 'waiting': Fore.BLUE,
3435 'reply': Fore.YELLOW,
3436 'lgtm': Fore.GREEN,
3437 'commit': Fore.MAGENTA,
3438 'closed': Fore.CYAN,
3439 'error': Fore.WHITE,
3440 }.get(status, Fore.WHITE)
3441
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003442
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003443def get_cl_statuses(changes, fine_grained, max_processes=None):
3444 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003445
3446 If fine_grained is true, this will fetch CL statuses from the server.
3447 Otherwise, simply indicate if there's a matching url for the given branches.
3448
3449 If max_processes is specified, it is used as the maximum number of processes
3450 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3451 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003452
3453 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003454 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003455 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003456 upload.verbosity = 0
3457
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003458 if not changes:
3459 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003460
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003461 if not fine_grained:
3462 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003463 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003464 for cl in changes:
3465 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003466 return
3467
3468 # First, sort out authentication issues.
3469 logging.debug('ensuring credentials exist')
3470 for cl in changes:
3471 cl.EnsureAuthenticated(force=False, refresh=True)
3472
3473 def fetch(cl):
3474 try:
3475 return (cl, cl.GetStatus())
3476 except:
3477 # See http://crbug.com/629863.
3478 logging.exception('failed to fetch status for %s:', cl)
3479 raise
3480
3481 threads_count = len(changes)
3482 if max_processes:
3483 threads_count = max(1, min(threads_count, max_processes))
3484 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3485
3486 pool = ThreadPool(threads_count)
3487 fetched_cls = set()
3488 try:
3489 it = pool.imap_unordered(fetch, changes).__iter__()
3490 while True:
3491 try:
3492 cl, status = it.next(timeout=5)
3493 except multiprocessing.TimeoutError:
3494 break
3495 fetched_cls.add(cl)
3496 yield cl, status
3497 finally:
3498 pool.close()
3499
3500 # Add any branches that failed to fetch.
3501 for cl in set(changes) - fetched_cls:
3502 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003503
rmistry@google.com2dd99862015-06-22 12:22:18 +00003504
3505def upload_branch_deps(cl, args):
3506 """Uploads CLs of local branches that are dependents of the current branch.
3507
3508 If the local branch dependency tree looks like:
3509 test1 -> test2.1 -> test3.1
3510 -> test3.2
3511 -> test2.2 -> test3.3
3512
3513 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3514 run on the dependent branches in this order:
3515 test2.1, test3.1, test3.2, test2.2, test3.3
3516
3517 Note: This function does not rebase your local dependent branches. Use it when
3518 you make a change to the parent branch that will not conflict with its
3519 dependent branches, and you would like their dependencies updated in
3520 Rietveld.
3521 """
3522 if git_common.is_dirty_git_tree('upload-branch-deps'):
3523 return 1
3524
3525 root_branch = cl.GetBranch()
3526 if root_branch is None:
3527 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3528 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003529 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003530 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3531 'patchset dependencies without an uploaded CL.')
3532
3533 branches = RunGit(['for-each-ref',
3534 '--format=%(refname:short) %(upstream:short)',
3535 'refs/heads'])
3536 if not branches:
3537 print('No local branches found.')
3538 return 0
3539
3540 # Create a dictionary of all local branches to the branches that are dependent
3541 # on it.
3542 tracked_to_dependents = collections.defaultdict(list)
3543 for b in branches.splitlines():
3544 tokens = b.split()
3545 if len(tokens) == 2:
3546 branch_name, tracked = tokens
3547 tracked_to_dependents[tracked].append(branch_name)
3548
vapiera7fbd5a2016-06-16 09:17:49 -07003549 print()
3550 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003551 dependents = []
3552 def traverse_dependents_preorder(branch, padding=''):
3553 dependents_to_process = tracked_to_dependents.get(branch, [])
3554 padding += ' '
3555 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003556 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003557 dependents.append(dependent)
3558 traverse_dependents_preorder(dependent, padding)
3559 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003560 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003561
3562 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003563 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003564 return 0
3565
vapiera7fbd5a2016-06-16 09:17:49 -07003566 print('This command will checkout all dependent branches and run '
3567 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003568 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3569
andybons@chromium.org962f9462016-02-03 20:00:42 +00003570 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003571 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003572 args.extend(['-t', 'Updated patchset dependency'])
3573
rmistry@google.com2dd99862015-06-22 12:22:18 +00003574 # Record all dependents that failed to upload.
3575 failures = {}
3576 # Go through all dependents, checkout the branch and upload.
3577 try:
3578 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003579 print()
3580 print('--------------------------------------')
3581 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003582 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003583 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003584 try:
3585 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003586 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003587 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003588 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003589 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003590 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003591 finally:
3592 # Swap back to the original root branch.
3593 RunGit(['checkout', '-q', root_branch])
3594
vapiera7fbd5a2016-06-16 09:17:49 -07003595 print()
3596 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003597 for dependent_branch in dependents:
3598 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003599 print(' %s : %s' % (dependent_branch, upload_status))
3600 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003601
3602 return 0
3603
3604
kmarshall3bff56b2016-06-06 18:31:47 -07003605def CMDarchive(parser, args):
3606 """Archives and deletes branches associated with closed changelists."""
3607 parser.add_option(
3608 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003609 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003610 parser.add_option(
3611 '-f', '--force', action='store_true',
3612 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003613 parser.add_option(
3614 '-d', '--dry-run', action='store_true',
3615 help='Skip the branch tagging and removal steps.')
3616 parser.add_option(
3617 '-t', '--notags', action='store_true',
3618 help='Do not tag archived branches. '
3619 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003620
3621 auth.add_auth_options(parser)
3622 options, args = parser.parse_args(args)
3623 if args:
3624 parser.error('Unsupported args: %s' % ' '.join(args))
3625 auth_config = auth.extract_auth_config_from_options(options)
3626
3627 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3628 if not branches:
3629 return 0
3630
vapiera7fbd5a2016-06-16 09:17:49 -07003631 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003632 changes = [Changelist(branchref=b, auth_config=auth_config)
3633 for b in branches.splitlines()]
3634 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3635 statuses = get_cl_statuses(changes,
3636 fine_grained=True,
3637 max_processes=options.maxjobs)
3638 proposal = [(cl.GetBranch(),
3639 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3640 for cl, status in statuses
3641 if status == 'closed']
3642 proposal.sort()
3643
3644 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003645 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003646 return 0
3647
3648 current_branch = GetCurrentBranch()
3649
vapiera7fbd5a2016-06-16 09:17:49 -07003650 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003651 if options.notags:
3652 for next_item in proposal:
3653 print(' ' + next_item[0])
3654 else:
3655 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3656 for next_item in proposal:
3657 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003658
kmarshall9249e012016-08-23 12:02:16 -07003659 # Quit now on precondition failure or if instructed by the user, either
3660 # via an interactive prompt or by command line flags.
3661 if options.dry_run:
3662 print('\nNo changes were made (dry run).\n')
3663 return 0
3664 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003665 print('You are currently on a branch \'%s\' which is associated with a '
3666 'closed codereview issue, so archive cannot proceed. Please '
3667 'checkout another branch and run this command again.' %
3668 current_branch)
3669 return 1
kmarshall9249e012016-08-23 12:02:16 -07003670 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003671 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3672 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003673 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003674 return 1
3675
3676 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003677 if not options.notags:
3678 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003679 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003680
vapiera7fbd5a2016-06-16 09:17:49 -07003681 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003682
3683 return 0
3684
3685
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003686def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003687 """Show status of changelists.
3688
3689 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003690 - Red not sent for review or broken
3691 - Blue waiting for review
3692 - Yellow waiting for you to reply to review
3693 - Green LGTM'ed
3694 - Magenta in the commit queue
3695 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003696
3697 Also see 'git cl comments'.
3698 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003699 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003700 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003701 parser.add_option('-f', '--fast', action='store_true',
3702 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003703 parser.add_option(
3704 '-j', '--maxjobs', action='store', type=int,
3705 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003706
3707 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003708 _add_codereview_issue_select_options(
3709 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003710 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003711 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003712 if args:
3713 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003714 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003715
iannuccie53c9352016-08-17 14:40:40 -07003716 if options.issue is not None and not options.field:
3717 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003718
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003719 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003720 cl = Changelist(auth_config=auth_config, issue=options.issue,
3721 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003722 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003723 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003724 elif options.field == 'id':
3725 issueid = cl.GetIssue()
3726 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003727 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003728 elif options.field == 'patch':
3729 patchset = cl.GetPatchset()
3730 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003731 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003732 elif options.field == 'status':
3733 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003734 elif options.field == 'url':
3735 url = cl.GetIssueURL()
3736 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003737 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003738 return 0
3739
3740 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3741 if not branches:
3742 print('No local branch found.')
3743 return 0
3744
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003745 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003746 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003747 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003748 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003749 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003750 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003751 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003752
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003753 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003754 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3755 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3756 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003757 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003758 c, status = output.next()
3759 branch_statuses[c.GetBranch()] = status
3760 status = branch_statuses.pop(branch)
3761 url = cl.GetIssueURL()
3762 if url and (not status or status == 'error'):
3763 # The issue probably doesn't exist anymore.
3764 url += ' (broken)'
3765
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003766 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003767 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003768 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003769 color = ''
3770 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003771 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003772 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003773 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003774 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003775
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003776
3777 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003778 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003779 print('Current branch: %s' % branch)
3780 for cl in changes:
3781 if cl.GetBranch() == branch:
3782 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003783 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003784 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003785 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003786 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003787 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003788 print('Issue description:')
3789 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003790 return 0
3791
3792
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003793def colorize_CMDstatus_doc():
3794 """To be called once in main() to add colors to git cl status help."""
3795 colors = [i for i in dir(Fore) if i[0].isupper()]
3796
3797 def colorize_line(line):
3798 for color in colors:
3799 if color in line.upper():
3800 # Extract whitespaces first and the leading '-'.
3801 indent = len(line) - len(line.lstrip(' ')) + 1
3802 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3803 return line
3804
3805 lines = CMDstatus.__doc__.splitlines()
3806 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3807
3808
phajdan.jre328cf92016-08-22 04:12:17 -07003809def write_json(path, contents):
3810 with open(path, 'w') as f:
3811 json.dump(contents, f)
3812
3813
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003814@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003815def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003816 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003817
3818 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003819 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003820 parser.add_option('-r', '--reverse', action='store_true',
3821 help='Lookup the branch(es) for the specified issues. If '
3822 'no issues are specified, all branches with mapped '
3823 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003824 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003825 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003826 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003827 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003828
dnj@chromium.org406c4402015-03-03 17:22:28 +00003829 if options.reverse:
3830 branches = RunGit(['for-each-ref', 'refs/heads',
3831 '--format=%(refname:short)']).splitlines()
3832
3833 # Reverse issue lookup.
3834 issue_branch_map = {}
3835 for branch in branches:
3836 cl = Changelist(branchref=branch)
3837 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3838 if not args:
3839 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003840 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003841 for issue in args:
3842 if not issue:
3843 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003844 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003845 print('Branch for issue number %s: %s' % (
3846 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003847 if options.json:
3848 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003849 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003850 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003851 if len(args) > 0:
3852 try:
3853 issue = int(args[0])
3854 except ValueError:
3855 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003856 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003857 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003858 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003859 if options.json:
3860 write_json(options.json, {
3861 'issue': cl.GetIssue(),
3862 'issue_url': cl.GetIssueURL(),
3863 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003864 return 0
3865
3866
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003867def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003868 """Shows or posts review comments for any changelist."""
3869 parser.add_option('-a', '--add-comment', dest='comment',
3870 help='comment to add to an issue')
3871 parser.add_option('-i', dest='issue',
3872 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003873 parser.add_option('-j', '--json-file',
3874 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003875 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003876 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003877 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003878
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003879 issue = None
3880 if options.issue:
3881 try:
3882 issue = int(options.issue)
3883 except ValueError:
3884 DieWithError('A review issue id is expected to be a number')
3885
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003886 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003887
3888 if options.comment:
3889 cl.AddComment(options.comment)
3890 return 0
3891
3892 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003893 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003894 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003895 summary.append({
3896 'date': message['date'],
3897 'lgtm': False,
3898 'message': message['text'],
3899 'not_lgtm': False,
3900 'sender': message['sender'],
3901 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003902 if message['disapproval']:
3903 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003904 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003905 elif message['approval']:
3906 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003907 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003908 elif message['sender'] == data['owner_email']:
3909 color = Fore.MAGENTA
3910 else:
3911 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003912 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003913 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003914 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003915 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003916 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003917 if options.json_file:
3918 with open(options.json_file, 'wb') as f:
3919 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003920 return 0
3921
3922
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003923@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003924def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003925 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003926 parser.add_option('-d', '--display', action='store_true',
3927 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003928 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003929 help='New description to set for this issue (- for stdin, '
3930 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003931 parser.add_option('-f', '--force', action='store_true',
3932 help='Delete any unpublished Gerrit edits for this issue '
3933 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003934
3935 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003936 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003937 options, args = parser.parse_args(args)
3938 _process_codereview_select_options(parser, options)
3939
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003940 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003941 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003942 target_issue_arg = ParseIssueNumberArgument(args[0])
3943 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003944 parser.print_help()
3945 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003946
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003947 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003948
martiniss6eda05f2016-06-30 10:18:35 -07003949 kwargs = {
3950 'auth_config': auth_config,
3951 'codereview': options.forced_codereview,
3952 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003953 if target_issue_arg:
3954 kwargs['issue'] = target_issue_arg.issue
3955 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07003956
3957 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003958
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003959 if not cl.GetIssue():
3960 DieWithError('This branch has no associated changelist.')
3961 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003962
smut@google.com34fb6b12015-07-13 20:03:26 +00003963 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003964 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003965 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003966
3967 if options.new_description:
3968 text = options.new_description
3969 if text == '-':
3970 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003971 elif text == '+':
3972 base_branch = cl.GetCommonAncestorWithUpstream()
3973 change = cl.GetChange(base_branch, None, local_description=True)
3974 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003975
3976 description.set_description(text)
3977 else:
3978 description.prompt()
3979
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003980 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003981 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003982 return 0
3983
3984
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003985def CreateDescriptionFromLog(args):
3986 """Pulls out the commit log to use as a base for the CL description."""
3987 log_args = []
3988 if len(args) == 1 and not args[0].endswith('.'):
3989 log_args = [args[0] + '..']
3990 elif len(args) == 1 and args[0].endswith('...'):
3991 log_args = [args[0][:-1]]
3992 elif len(args) == 2:
3993 log_args = [args[0] + '..' + args[1]]
3994 else:
3995 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003996 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003997
3998
thestig@chromium.org44202a22014-03-11 19:22:18 +00003999def CMDlint(parser, args):
4000 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004001 parser.add_option('--filter', action='append', metavar='-x,+y',
4002 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004003 auth.add_auth_options(parser)
4004 options, args = parser.parse_args(args)
4005 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004006
4007 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004008 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004009 try:
4010 import cpplint
4011 import cpplint_chromium
4012 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004013 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004014 return 1
4015
4016 # Change the current working directory before calling lint so that it
4017 # shows the correct base.
4018 previous_cwd = os.getcwd()
4019 os.chdir(settings.GetRoot())
4020 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004021 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004022 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4023 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004024 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004025 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004026 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004027
4028 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004029 command = args + files
4030 if options.filter:
4031 command = ['--filter=' + ','.join(options.filter)] + command
4032 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004033
4034 white_regex = re.compile(settings.GetLintRegex())
4035 black_regex = re.compile(settings.GetLintIgnoreRegex())
4036 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4037 for filename in filenames:
4038 if white_regex.match(filename):
4039 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004040 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004041 else:
4042 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4043 extra_check_functions)
4044 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004045 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004046 finally:
4047 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004048 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004049 if cpplint._cpplint_state.error_count != 0:
4050 return 1
4051 return 0
4052
4053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004054def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004055 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004056 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004057 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004058 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004059 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004060 auth.add_auth_options(parser)
4061 options, args = parser.parse_args(args)
4062 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063
sbc@chromium.org71437c02015-04-09 19:29:40 +00004064 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004065 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004066 return 1
4067
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004068 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004069 if args:
4070 base_branch = args[0]
4071 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004072 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004073 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004074
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004075 cl.RunHook(
4076 committing=not options.upload,
4077 may_prompt=False,
4078 verbose=options.verbose,
4079 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004080 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004081
4082
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004083def GenerateGerritChangeId(message):
4084 """Returns Ixxxxxx...xxx change id.
4085
4086 Works the same way as
4087 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4088 but can be called on demand on all platforms.
4089
4090 The basic idea is to generate git hash of a state of the tree, original commit
4091 message, author/committer info and timestamps.
4092 """
4093 lines = []
4094 tree_hash = RunGitSilent(['write-tree'])
4095 lines.append('tree %s' % tree_hash.strip())
4096 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4097 if code == 0:
4098 lines.append('parent %s' % parent.strip())
4099 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4100 lines.append('author %s' % author.strip())
4101 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4102 lines.append('committer %s' % committer.strip())
4103 lines.append('')
4104 # Note: Gerrit's commit-hook actually cleans message of some lines and
4105 # whitespace. This code is not doing this, but it clearly won't decrease
4106 # entropy.
4107 lines.append(message)
4108 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4109 stdin='\n'.join(lines))
4110 return 'I%s' % change_hash.strip()
4111
4112
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004113def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004114 """Computes the remote branch ref to use for the CL.
4115
4116 Args:
4117 remote (str): The git remote for the CL.
4118 remote_branch (str): The git remote branch for the CL.
4119 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004120 """
4121 if not (remote and remote_branch):
4122 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004123
wittman@chromium.org455dc922015-01-26 20:15:50 +00004124 if target_branch:
4125 # Cannonicalize branch references to the equivalent local full symbolic
4126 # refs, which are then translated into the remote full symbolic refs
4127 # below.
4128 if '/' not in target_branch:
4129 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4130 else:
4131 prefix_replacements = (
4132 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4133 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4134 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4135 )
4136 match = None
4137 for regex, replacement in prefix_replacements:
4138 match = re.search(regex, target_branch)
4139 if match:
4140 remote_branch = target_branch.replace(match.group(0), replacement)
4141 break
4142 if not match:
4143 # This is a branch path but not one we recognize; use as-is.
4144 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004145 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4146 # Handle the refs that need to land in different refs.
4147 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004148
wittman@chromium.org455dc922015-01-26 20:15:50 +00004149 # Create the true path to the remote branch.
4150 # Does the following translation:
4151 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4152 # * refs/remotes/origin/master -> refs/heads/master
4153 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4154 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4155 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4156 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4157 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4158 'refs/heads/')
4159 elif remote_branch.startswith('refs/remotes/branch-heads'):
4160 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004161
wittman@chromium.org455dc922015-01-26 20:15:50 +00004162 return remote_branch
4163
4164
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004165def cleanup_list(l):
4166 """Fixes a list so that comma separated items are put as individual items.
4167
4168 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4169 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4170 """
4171 items = sum((i.split(',') for i in l), [])
4172 stripped_items = (i.strip() for i in items)
4173 return sorted(filter(None, stripped_items))
4174
4175
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004176@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004177def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004178 """Uploads the current changelist to codereview.
4179
4180 Can skip dependency patchset uploads for a branch by running:
4181 git config branch.branch_name.skip-deps-uploads True
4182 To unset run:
4183 git config --unset branch.branch_name.skip-deps-uploads
4184 Can also set the above globally by using the --global flag.
4185 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004186 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4187 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004188 parser.add_option('--bypass-watchlists', action='store_true',
4189 dest='bypass_watchlists',
4190 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004191 parser.add_option('-f', action='store_true', dest='force',
4192 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004193 parser.add_option('--message', '-m', dest='message',
4194 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004195 parser.add_option('-b', '--bug',
4196 help='pre-populate the bug number(s) for this issue. '
4197 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004198 parser.add_option('--message-file', dest='message_file',
4199 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004200 parser.add_option('--title', '-t', dest='title',
4201 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004202 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004203 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004204 help='reviewer email addresses')
4205 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004206 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004207 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004208 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004209 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004210 parser.add_option('--emulate_svn_auto_props',
4211 '--emulate-svn-auto-props',
4212 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004213 dest="emulate_svn_auto_props",
4214 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004215 parser.add_option('-c', '--use-commit-queue', action='store_true',
4216 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004217 parser.add_option('--private', action='store_true',
4218 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004219 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004220 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004221 metavar='TARGET',
4222 help='Apply CL to remote ref TARGET. ' +
4223 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004224 parser.add_option('--squash', action='store_true',
4225 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004226 parser.add_option('--no-squash', action='store_true',
4227 help='Don\'t squash multiple commits into one ' +
4228 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004229 parser.add_option('--topic', default=None,
4230 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004231 parser.add_option('--email', default=None,
4232 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004233 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4234 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004235 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4236 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004237 help='Send the patchset to do a CQ dry run right after '
4238 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004239 parser.add_option('--dependencies', action='store_true',
4240 help='Uploads CLs of all the local branches that depend on '
4241 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004242
rmistry@google.com2dd99862015-06-22 12:22:18 +00004243 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004244 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004245 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004246 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004247 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004248 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004249 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004250
sbc@chromium.org71437c02015-04-09 19:29:40 +00004251 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004252 return 1
4253
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004254 options.reviewers = cleanup_list(options.reviewers)
4255 options.cc = cleanup_list(options.cc)
4256
tandriib80458a2016-06-23 12:20:07 -07004257 if options.message_file:
4258 if options.message:
4259 parser.error('only one of --message and --message-file allowed.')
4260 options.message = gclient_utils.FileRead(options.message_file)
4261 options.message_file = None
4262
tandrii4d0545a2016-07-06 03:56:49 -07004263 if options.cq_dry_run and options.use_commit_queue:
4264 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4265
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004266 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4267 settings.GetIsGerrit()
4268
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004269 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004270 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004271
4272
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004273@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004274def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004275 """DEPRECATED: Used to commit the current changelist via git-svn."""
4276 message = ('git-cl no longer supports committing to SVN repositories via '
4277 'git-svn. You probably want to use `git cl land` instead.')
4278 print(message)
4279 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004280
4281
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004282@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004283def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004284 """Commits the current changelist via git.
4285
4286 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4287 upstream and closes the issue automatically and atomically.
4288
4289 Otherwise (in case of Rietveld):
4290 Squashes branch into a single commit.
4291 Updates commit message with metadata (e.g. pointer to review).
4292 Pushes the code upstream.
4293 Updates review and closes.
4294 """
4295 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4296 help='bypass upload presubmit hook')
4297 parser.add_option('-m', dest='message',
4298 help="override review description")
4299 parser.add_option('-f', action='store_true', dest='force',
4300 help="force yes to questions (don't prompt)")
4301 parser.add_option('-c', dest='contributor',
4302 help="external contributor for patch (appended to " +
4303 "description and used as author for git). Should be " +
4304 "formatted as 'First Last <email@example.com>'")
4305 add_git_similarity(parser)
4306 auth.add_auth_options(parser)
4307 (options, args) = parser.parse_args(args)
4308 auth_config = auth.extract_auth_config_from_options(options)
4309
4310 cl = Changelist(auth_config=auth_config)
4311
4312 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4313 if cl.IsGerrit():
4314 if options.message:
4315 # This could be implemented, but it requires sending a new patch to
4316 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4317 # Besides, Gerrit has the ability to change the commit message on submit
4318 # automatically, thus there is no need to support this option (so far?).
4319 parser.error('-m MESSAGE option is not supported for Gerrit.')
4320 if options.contributor:
4321 parser.error(
4322 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4323 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4324 'the contributor\'s "name <email>". If you can\'t upload such a '
4325 'commit for review, contact your repository admin and request'
4326 '"Forge-Author" permission.')
4327 if not cl.GetIssue():
4328 DieWithError('You must upload the change first to Gerrit.\n'
4329 ' If you would rather have `git cl land` upload '
4330 'automatically for you, see http://crbug.com/642759')
4331 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4332 options.verbose)
4333
4334 current = cl.GetBranch()
4335 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4336 if remote == '.':
4337 print()
4338 print('Attempting to push branch %r into another local branch!' % current)
4339 print()
4340 print('Either reparent this branch on top of origin/master:')
4341 print(' git reparent-branch --root')
4342 print()
4343 print('OR run `git rebase-update` if you think the parent branch is ')
4344 print('already committed.')
4345 print()
4346 print(' Current parent: %r' % upstream_branch)
4347 return 1
4348
4349 if not args:
4350 # Default to merging against our best guess of the upstream branch.
4351 args = [cl.GetUpstreamBranch()]
4352
4353 if options.contributor:
4354 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4355 print("Please provide contibutor as 'First Last <email@example.com>'")
4356 return 1
4357
4358 base_branch = args[0]
4359
4360 if git_common.is_dirty_git_tree('land'):
4361 return 1
4362
4363 # This rev-list syntax means "show all commits not in my branch that
4364 # are in base_branch".
4365 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4366 base_branch]).splitlines()
4367 if upstream_commits:
4368 print('Base branch "%s" has %d commits '
4369 'not in this branch.' % (base_branch, len(upstream_commits)))
4370 print('Run "git merge %s" before attempting to land.' % base_branch)
4371 return 1
4372
4373 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4374 if not options.bypass_hooks:
4375 author = None
4376 if options.contributor:
4377 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4378 hook_results = cl.RunHook(
4379 committing=True,
4380 may_prompt=not options.force,
4381 verbose=options.verbose,
4382 change=cl.GetChange(merge_base, author))
4383 if not hook_results.should_continue():
4384 return 1
4385
4386 # Check the tree status if the tree status URL is set.
4387 status = GetTreeStatus()
4388 if 'closed' == status:
4389 print('The tree is closed. Please wait for it to reopen. Use '
4390 '"git cl land --bypass-hooks" to commit on a closed tree.')
4391 return 1
4392 elif 'unknown' == status:
4393 print('Unable to determine tree status. Please verify manually and '
4394 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4395 return 1
4396
4397 change_desc = ChangeDescription(options.message)
4398 if not change_desc.description and cl.GetIssue():
4399 change_desc = ChangeDescription(cl.GetDescription())
4400
4401 if not change_desc.description:
4402 if not cl.GetIssue() and options.bypass_hooks:
4403 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4404 else:
4405 print('No description set.')
4406 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4407 return 1
4408
4409 # Keep a separate copy for the commit message, because the commit message
4410 # contains the link to the Rietveld issue, while the Rietveld message contains
4411 # the commit viewvc url.
4412 if cl.GetIssue():
4413 change_desc.update_reviewers(cl.GetApprovingReviewers())
4414
4415 commit_desc = ChangeDescription(change_desc.description)
4416 if cl.GetIssue():
4417 # Xcode won't linkify this URL unless there is a non-whitespace character
4418 # after it. Add a period on a new line to circumvent this. Also add a space
4419 # before the period to make sure that Gitiles continues to correctly resolve
4420 # the URL.
4421 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4422 if options.contributor:
4423 commit_desc.append_footer('Patch from %s.' % options.contributor)
4424
4425 print('Description:')
4426 print(commit_desc.description)
4427
4428 branches = [merge_base, cl.GetBranchRef()]
4429 if not options.force:
4430 print_stats(options.similarity, options.find_copies, branches)
4431
4432 # We want to squash all this branch's commits into one commit with the proper
4433 # description. We do this by doing a "reset --soft" to the base branch (which
4434 # keeps the working copy the same), then landing that.
4435 MERGE_BRANCH = 'git-cl-commit'
4436 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4437 # Delete the branches if they exist.
4438 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4439 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4440 result = RunGitWithCode(showref_cmd)
4441 if result[0] == 0:
4442 RunGit(['branch', '-D', branch])
4443
4444 # We might be in a directory that's present in this branch but not in the
4445 # trunk. Move up to the top of the tree so that git commands that expect a
4446 # valid CWD won't fail after we check out the merge branch.
4447 rel_base_path = settings.GetRelativeRoot()
4448 if rel_base_path:
4449 os.chdir(rel_base_path)
4450
4451 # Stuff our change into the merge branch.
4452 # We wrap in a try...finally block so if anything goes wrong,
4453 # we clean up the branches.
4454 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004455 revision = None
4456 try:
4457 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4458 RunGit(['reset', '--soft', merge_base])
4459 if options.contributor:
4460 RunGit(
4461 [
4462 'commit', '--author', options.contributor,
4463 '-m', commit_desc.description,
4464 ])
4465 else:
4466 RunGit(['commit', '-m', commit_desc.description])
4467
4468 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4469 mirror = settings.GetGitMirror(remote)
4470 if mirror:
4471 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004472 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004473 else:
4474 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004475 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004476 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4477
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004478 if git_numberer_enabled:
4479 # TODO(tandrii): maybe do autorebase + retry on failure
4480 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004481 logging.debug('Adding git number footers')
4482 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4483 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4484 branch)
4485 # Ensure timestamps are monotonically increasing.
4486 timestamp = max(1 + _get_committer_timestamp(merge_base),
4487 _get_committer_timestamp('HEAD'))
4488 _git_amend_head(commit_desc.description, timestamp)
4489 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004490
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004491 retcode, output = RunGitWithCode(
4492 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004493 if retcode == 0:
4494 revision = RunGit(['rev-parse', 'HEAD']).strip()
4495 logging.debug(output)
4496 except: # pylint: disable=bare-except
4497 if _IS_BEING_TESTED:
4498 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4499 + '-' * 30 + '8<' + '-' * 30)
4500 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4501 raise
4502 finally:
4503 # And then swap back to the original branch and clean up.
4504 RunGit(['checkout', '-q', cl.GetBranch()])
4505 RunGit(['branch', '-D', MERGE_BRANCH])
4506
4507 if not revision:
4508 print('Failed to push. If this persists, please file a bug.')
4509 return 1
4510
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004511 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004512 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004513 if viewvc_url and revision:
4514 change_desc.append_footer(
4515 'Committed: %s%s' % (viewvc_url, revision))
4516 elif revision:
4517 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004518 print('Closing issue '
4519 '(you may be prompted for your codereview password)...')
4520 cl.UpdateDescription(change_desc.description)
4521 cl.CloseIssue()
4522 props = cl.GetIssueProperties()
4523 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004524 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4525 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004526 if options.bypass_hooks:
4527 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4528 else:
4529 comment += ' (presubmit successful).'
4530 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4531
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004532 if os.path.isfile(POSTUPSTREAM_HOOK):
4533 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4534
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004535 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004536
4537
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004538@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004539def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004540 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004541 parser.add_option('-b', dest='newbranch',
4542 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004543 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004544 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004545 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4546 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004547 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004548 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004549 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004550 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004551 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004552 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004553
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004554
4555 group = optparse.OptionGroup(
4556 parser,
4557 'Options for continuing work on the current issue uploaded from a '
4558 'different clone (e.g. different machine). Must be used independently '
4559 'from the other options. No issue number should be specified, and the '
4560 'branch must have an issue number associated with it')
4561 group.add_option('--reapply', action='store_true', dest='reapply',
4562 help='Reset the branch and reapply the issue.\n'
4563 'CAUTION: This will undo any local changes in this '
4564 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004565
4566 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004567 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004568 parser.add_option_group(group)
4569
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004570 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004571 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004572 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004573 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004574 auth_config = auth.extract_auth_config_from_options(options)
4575
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004576
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004577 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004578 if options.newbranch:
4579 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004580 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004581 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004582
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004583 cl = Changelist(auth_config=auth_config,
4584 codereview=options.forced_codereview)
4585 if not cl.GetIssue():
4586 parser.error('current branch must have an associated issue')
4587
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004588 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004589 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004590 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004591
4592 RunGit(['reset', '--hard', upstream])
4593 if options.pull:
4594 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004595
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004596 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4597 options.directory)
4598
4599 if len(args) != 1 or not args[0]:
4600 parser.error('Must specify issue number or url')
4601
4602 # We don't want uncommitted changes mixed up with the patch.
4603 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004604 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004605
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004606 if options.newbranch:
4607 if options.force:
4608 RunGit(['branch', '-D', options.newbranch],
4609 stderr=subprocess2.PIPE, error_ok=True)
4610 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004611 elif not GetCurrentBranch():
4612 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004613
4614 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4615
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004616 if cl.IsGerrit():
4617 if options.reject:
4618 parser.error('--reject is not supported with Gerrit codereview.')
4619 if options.nocommit:
4620 parser.error('--nocommit is not supported with Gerrit codereview.')
4621 if options.directory:
4622 parser.error('--directory is not supported with Gerrit codereview.')
4623
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004624 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004625 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004626
4627
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004628def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004629 """Fetches the tree status and returns either 'open', 'closed',
4630 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004631 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004632 if url:
4633 status = urllib2.urlopen(url).read().lower()
4634 if status.find('closed') != -1 or status == '0':
4635 return 'closed'
4636 elif status.find('open') != -1 or status == '1':
4637 return 'open'
4638 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004639 return 'unset'
4640
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004641
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004642def GetTreeStatusReason():
4643 """Fetches the tree status from a json url and returns the message
4644 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004645 url = settings.GetTreeStatusUrl()
4646 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004647 connection = urllib2.urlopen(json_url)
4648 status = json.loads(connection.read())
4649 connection.close()
4650 return status['message']
4651
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004652
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004653def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004654 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004655 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004656 status = GetTreeStatus()
4657 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004658 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004659 return 2
4660
vapiera7fbd5a2016-06-16 09:17:49 -07004661 print('The tree is %s' % status)
4662 print()
4663 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004664 if status != 'open':
4665 return 1
4666 return 0
4667
4668
maruel@chromium.org15192402012-09-06 12:38:29 +00004669def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004670 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004671 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004672 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004673 '-b', '--bot', action='append',
4674 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4675 'times to specify multiple builders. ex: '
4676 '"-b win_rel -b win_layout". See '
4677 'the try server waterfall for the builders name and the tests '
4678 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004679 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004680 '-B', '--bucket', default='',
4681 help=('Buildbucket bucket to send the try requests.'))
4682 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004683 '-m', '--master', default='',
4684 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004685 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004686 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004687 help='Revision to use for the try job; default: the revision will '
4688 'be determined by the try recipe that builder runs, which usually '
4689 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004690 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004691 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004692 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004693 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004694 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004695 '--project',
4696 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004697 'in recipe to determine to which repository or directory to '
4698 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004699 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004700 '-p', '--property', dest='properties', action='append', default=[],
4701 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004702 'key2=value2 etc. The value will be treated as '
4703 'json if decodable, or as string otherwise. '
4704 'NOTE: using this may make your try job not usable for CQ, '
4705 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004706 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004707 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4708 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004709 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004710 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004711 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004712 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004713
machenbach@chromium.org45453142015-09-15 08:45:22 +00004714 # Make sure that all properties are prop=value pairs.
4715 bad_params = [x for x in options.properties if '=' not in x]
4716 if bad_params:
4717 parser.error('Got properties with missing "=": %s' % bad_params)
4718
maruel@chromium.org15192402012-09-06 12:38:29 +00004719 if args:
4720 parser.error('Unknown arguments: %s' % args)
4721
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004722 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004723 if not cl.GetIssue():
4724 parser.error('Need to upload first')
4725
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004726 if cl.IsGerrit():
4727 # HACK: warm up Gerrit change detail cache to save on RPCs.
4728 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4729
tandriie113dfd2016-10-11 10:20:12 -07004730 error_message = cl.CannotTriggerTryJobReason()
4731 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004732 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004733
borenet6c0efe62016-10-19 08:13:29 -07004734 if options.bucket and options.master:
4735 parser.error('Only one of --bucket and --master may be used.')
4736
qyearsley1fdfcb62016-10-24 13:22:03 -07004737 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004738
qyearsleydd49f942016-10-28 11:57:22 -07004739 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4740 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004741 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004742 if options.verbose:
4743 print('git cl try with no bots now defaults to CQ Dry Run.')
4744 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004745
borenet6c0efe62016-10-19 08:13:29 -07004746 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004747 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004748 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004749 'of bot requires an initial job from a parent (usually a builder). '
4750 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004751 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004752 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004753
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004754 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004755 # TODO(tandrii): Checking local patchset against remote patchset is only
4756 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4757 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004758 print('Warning: Codereview server has newer patchsets (%s) than most '
4759 'recent upload from local checkout (%s). Did a previous upload '
4760 'fail?\n'
4761 'By default, git cl try uses the latest patchset from '
4762 'codereview, continuing to use patchset %s.\n' %
4763 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004764
tandrii568043b2016-10-11 07:49:18 -07004765 try:
borenet6c0efe62016-10-19 08:13:29 -07004766 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4767 patchset)
tandrii568043b2016-10-11 07:49:18 -07004768 except BuildbucketResponseException as ex:
4769 print('ERROR: %s' % ex)
4770 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004771 return 0
4772
4773
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004774def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004775 """Prints info about try jobs associated with current CL."""
4776 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004777 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004778 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004779 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004780 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004781 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004782 '--color', action='store_true', default=setup_color.IS_TTY,
4783 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004784 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004785 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4786 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004787 group.add_option(
4788 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004789 parser.add_option_group(group)
4790 auth.add_auth_options(parser)
4791 options, args = parser.parse_args(args)
4792 if args:
4793 parser.error('Unrecognized args: %s' % ' '.join(args))
4794
4795 auth_config = auth.extract_auth_config_from_options(options)
4796 cl = Changelist(auth_config=auth_config)
4797 if not cl.GetIssue():
4798 parser.error('Need to upload first')
4799
tandrii221ab252016-10-06 08:12:04 -07004800 patchset = options.patchset
4801 if not patchset:
4802 patchset = cl.GetMostRecentPatchset()
4803 if not patchset:
4804 parser.error('Codereview doesn\'t know about issue %s. '
4805 'No access to issue or wrong issue number?\n'
4806 'Either upload first, or pass --patchset explicitely' %
4807 cl.GetIssue())
4808
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004809 # TODO(tandrii): Checking local patchset against remote patchset is only
4810 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4811 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004812 print('Warning: Codereview server has newer patchsets (%s) than most '
4813 'recent upload from local checkout (%s). Did a previous upload '
4814 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004815 'By default, git cl try-results uses the latest patchset from '
4816 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004817 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004818 try:
tandrii221ab252016-10-06 08:12:04 -07004819 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004820 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004821 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004822 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004823 if options.json:
4824 write_try_results_json(options.json, jobs)
4825 else:
4826 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004827 return 0
4828
4829
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004830@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004831def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004832 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004833 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004834 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004835 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004836
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004837 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004838 if args:
4839 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004840 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004841 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004842 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004843 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004844
4845 # Clear configured merge-base, if there is one.
4846 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004847 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004848 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004849 return 0
4850
4851
thestig@chromium.org00858c82013-12-02 23:08:03 +00004852def CMDweb(parser, args):
4853 """Opens the current CL in the web browser."""
4854 _, args = parser.parse_args(args)
4855 if args:
4856 parser.error('Unrecognized args: %s' % ' '.join(args))
4857
4858 issue_url = Changelist().GetIssueURL()
4859 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004860 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004861 return 1
4862
4863 webbrowser.open(issue_url)
4864 return 0
4865
4866
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004867def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004868 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004869 parser.add_option('-d', '--dry-run', action='store_true',
4870 help='trigger in dry run mode')
4871 parser.add_option('-c', '--clear', action='store_true',
4872 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004873 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004874 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004875 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004876 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004877 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004878 if args:
4879 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004880 if options.dry_run and options.clear:
4881 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4882
iannuccie53c9352016-08-17 14:40:40 -07004883 cl = Changelist(auth_config=auth_config, issue=options.issue,
4884 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004885 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004886 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004887 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004888 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004889 state = _CQState.DRY_RUN
4890 else:
4891 state = _CQState.COMMIT
4892 if not cl.GetIssue():
4893 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004894 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004895 return 0
4896
4897
groby@chromium.org411034a2013-02-26 15:12:01 +00004898def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004899 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004900 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004901 auth.add_auth_options(parser)
4902 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004903 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004904 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004905 if args:
4906 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004907 cl = Changelist(auth_config=auth_config, issue=options.issue,
4908 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004909 # Ensure there actually is an issue to close.
4910 cl.GetDescription()
4911 cl.CloseIssue()
4912 return 0
4913
4914
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004915def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004916 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004917 parser.add_option(
4918 '--stat',
4919 action='store_true',
4920 dest='stat',
4921 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004922 auth.add_auth_options(parser)
4923 options, args = parser.parse_args(args)
4924 auth_config = auth.extract_auth_config_from_options(options)
4925 if args:
4926 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004927
4928 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004929 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004930 # Staged changes would be committed along with the patch from last
4931 # upload, hence counted toward the "last upload" side in the final
4932 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004933 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004934 return 1
4935
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004936 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004937 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004938 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004939 if not issue:
4940 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004941 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004942 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004943
4944 # Create a new branch based on the merge-base
4945 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004946 # Clear cached branch in cl object, to avoid overwriting original CL branch
4947 # properties.
4948 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004949 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004950 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004951 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004952 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004953 return rtn
4954
wychen@chromium.org06928532015-02-03 02:11:29 +00004955 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004956 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004957 cmd = ['git', 'diff']
4958 if options.stat:
4959 cmd.append('--stat')
4960 cmd.extend([TMP_BRANCH, branch, '--'])
4961 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004962 finally:
4963 RunGit(['checkout', '-q', branch])
4964 RunGit(['branch', '-D', TMP_BRANCH])
4965
4966 return 0
4967
4968
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004969def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004970 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004971 parser.add_option(
4972 '--no-color',
4973 action='store_true',
4974 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004975 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004976 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004977 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004978
4979 author = RunGit(['config', 'user.email']).strip() or None
4980
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004981 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004982
4983 if args:
4984 if len(args) > 1:
4985 parser.error('Unknown args')
4986 base_branch = args[0]
4987 else:
4988 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004989 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004990
4991 change = cl.GetChange(base_branch, None)
4992 return owners_finder.OwnersFinder(
4993 [f.LocalPath() for f in
4994 cl.GetChange(base_branch, None).AffectedFiles()],
4995 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004996 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004997 disable_color=options.no_color).run()
4998
4999
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005000def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005001 """Generates a diff command."""
5002 # Generate diff for the current branch's changes.
5003 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005004 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005005
5006 if args:
5007 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005008 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005009 diff_cmd.append(arg)
5010 else:
5011 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005012
5013 return diff_cmd
5014
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005015
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005016def MatchingFileType(file_name, extensions):
5017 """Returns true if the file name ends with one of the given extensions."""
5018 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005019
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005020
enne@chromium.org555cfe42014-01-29 18:21:39 +00005021@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005022def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005023 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005024 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005025 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005026 parser.add_option('--full', action='store_true',
5027 help='Reformat the full content of all touched files')
5028 parser.add_option('--dry-run', action='store_true',
5029 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005030 parser.add_option('--python', action='store_true',
5031 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005032 parser.add_option('--js', action='store_true',
5033 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005034 parser.add_option('--diff', action='store_true',
5035 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005036 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005037
Daniel Chengc55eecf2016-12-30 03:11:02 -08005038 # Normalize any remaining args against the current path, so paths relative to
5039 # the current directory are still resolved as expected.
5040 args = [os.path.join(os.getcwd(), arg) for arg in args]
5041
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005042 # git diff generates paths against the root of the repository. Change
5043 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005044 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005045 if rel_base_path:
5046 os.chdir(rel_base_path)
5047
digit@chromium.org29e47272013-05-17 17:01:46 +00005048 # Grab the merge-base commit, i.e. the upstream commit of the current
5049 # branch when it was created or the last time it was rebased. This is
5050 # to cover the case where the user may have called "git fetch origin",
5051 # moving the origin branch to a newer commit, but hasn't rebased yet.
5052 upstream_commit = None
5053 cl = Changelist()
5054 upstream_branch = cl.GetUpstreamBranch()
5055 if upstream_branch:
5056 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5057 upstream_commit = upstream_commit.strip()
5058
5059 if not upstream_commit:
5060 DieWithError('Could not find base commit for this branch. '
5061 'Are you in detached state?')
5062
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005063 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5064 diff_output = RunGit(changed_files_cmd)
5065 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005066 # Filter out files deleted by this CL
5067 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005068
Christopher Lamc5ba6922017-01-24 11:19:14 +11005069 if opts.js:
5070 CLANG_EXTS.append('.js')
5071
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005072 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5073 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5074 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005075 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005076
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005077 top_dir = os.path.normpath(
5078 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5079
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005080 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5081 # formatted. This is used to block during the presubmit.
5082 return_value = 0
5083
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005084 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005085 # Locate the clang-format binary in the checkout
5086 try:
5087 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005088 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005089 DieWithError(e)
5090
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005091 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005092 cmd = [clang_format_tool]
5093 if not opts.dry_run and not opts.diff:
5094 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005095 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005096 if opts.diff:
5097 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005098 else:
5099 env = os.environ.copy()
5100 env['PATH'] = str(os.path.dirname(clang_format_tool))
5101 try:
5102 script = clang_format.FindClangFormatScriptInChromiumTree(
5103 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005104 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005105 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005106
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005107 cmd = [sys.executable, script, '-p0']
5108 if not opts.dry_run and not opts.diff:
5109 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005110
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005111 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5112 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005113
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005114 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5115 if opts.diff:
5116 sys.stdout.write(stdout)
5117 if opts.dry_run and len(stdout) > 0:
5118 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005119
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005120 # Similar code to above, but using yapf on .py files rather than clang-format
5121 # on C/C++ files
5122 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005123 yapf_tool = gclient_utils.FindExecutable('yapf')
5124 if yapf_tool is None:
5125 DieWithError('yapf not found in PATH')
5126
5127 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005128 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005129 cmd = [yapf_tool]
5130 if not opts.dry_run and not opts.diff:
5131 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005132 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005133 if opts.diff:
5134 sys.stdout.write(stdout)
5135 else:
5136 # TODO(sbc): yapf --lines mode still has some issues.
5137 # https://github.com/google/yapf/issues/154
5138 DieWithError('--python currently only works with --full')
5139
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005140 # Dart's formatter does not have the nice property of only operating on
5141 # modified chunks, so hard code full.
5142 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005143 try:
5144 command = [dart_format.FindDartFmtToolInChromiumTree()]
5145 if not opts.dry_run and not opts.diff:
5146 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005147 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005148
ppi@chromium.org6593d932016-03-03 15:41:15 +00005149 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005150 if opts.dry_run and stdout:
5151 return_value = 2
5152 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005153 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5154 'found in this checkout. Files in other languages are still '
5155 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005156
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005157 # Format GN build files. Always run on full build files for canonical form.
5158 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005159 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005160 if opts.dry_run or opts.diff:
5161 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005162 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005163 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5164 shell=sys.platform == 'win32',
5165 cwd=top_dir)
5166 if opts.dry_run and gn_ret == 2:
5167 return_value = 2 # Not formatted.
5168 elif opts.diff and gn_ret == 2:
5169 # TODO this should compute and print the actual diff.
5170 print("This change has GN build file diff for " + gn_diff_file)
5171 elif gn_ret != 0:
5172 # For non-dry run cases (and non-2 return values for dry-run), a
5173 # nonzero error code indicates a failure, probably because the file
5174 # doesn't parse.
5175 DieWithError("gn format failed on " + gn_diff_file +
5176 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005177
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005178 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005179
5180
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005181@subcommand.usage('<codereview url or issue id>')
5182def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005183 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005184 _, args = parser.parse_args(args)
5185
5186 if len(args) != 1:
5187 parser.print_help()
5188 return 1
5189
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005190 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005191 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005192 parser.print_help()
5193 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005194 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005195
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005196 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005197 output = RunGit(['config', '--local', '--get-regexp',
5198 r'branch\..*\.%s' % issueprefix],
5199 error_ok=True)
5200 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005201 if issue == target_issue:
5202 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005203
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005204 branches = []
5205 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005206 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005207 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005208 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005209 return 1
5210 if len(branches) == 1:
5211 RunGit(['checkout', branches[0]])
5212 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005213 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005214 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005215 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005216 which = raw_input('Choose by index: ')
5217 try:
5218 RunGit(['checkout', branches[int(which)]])
5219 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005220 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005221 return 1
5222
5223 return 0
5224
5225
maruel@chromium.org29404b52014-09-08 22:58:00 +00005226def CMDlol(parser, args):
5227 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005228 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005229 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5230 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5231 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005232 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005233 return 0
5234
5235
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005236class OptionParser(optparse.OptionParser):
5237 """Creates the option parse and add --verbose support."""
5238 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005239 optparse.OptionParser.__init__(
5240 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005241 self.add_option(
5242 '-v', '--verbose', action='count', default=0,
5243 help='Use 2 times for more debugging info')
5244
5245 def parse_args(self, args=None, values=None):
5246 options, args = optparse.OptionParser.parse_args(self, args, values)
5247 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005248 logging.basicConfig(
5249 level=levels[min(options.verbose, len(levels) - 1)],
5250 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5251 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005252 return options, args
5253
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005254
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005255def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005256 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005257 print('\nYour python version %s is unsupported, please upgrade.\n' %
5258 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005259 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005260
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005261 # Reload settings.
5262 global settings
5263 settings = Settings()
5264
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005265 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005266 dispatcher = subcommand.CommandDispatcher(__name__)
5267 try:
5268 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005269 except auth.AuthenticationError as e:
5270 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005271 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005272 if e.code != 500:
5273 raise
5274 DieWithError(
5275 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5276 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005277 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005278
5279
5280if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005281 # These affect sys.stdout so do it outside of main() to simplify mocks in
5282 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005283 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005284 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005285 try:
5286 sys.exit(main(sys.argv[1:]))
5287 except KeyboardInterrupt:
5288 sys.stderr.write('interrupted\n')
5289 sys.exit(1)