blob: 8856fd7ab21dba2084e5dcc9bd77277deb9fad4d [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
466 # TODO(tandrii): consider caching Gerrit CL details just like
467 # _RietveldChangelistImpl does, then caching values in these two variables
468 # won't be necessary.
469 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000470
471 buildbucket_put_url = (
472 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000473 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700474 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
475 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
476 hostname=codereview_host,
477 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000478 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700479
480 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
481 shared_parameters_properties['category'] = category
482 if options.clobber:
483 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700484 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700485 if extra_properties:
486 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000487
488 batch_req_body = {'builds': []}
489 print_text = []
490 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700491 for bucket, builders_and_tests in sorted(buckets.iteritems()):
492 print_text.append('Bucket: %s' % bucket)
493 master = None
494 if bucket.startswith(MASTER_PREFIX):
495 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000496 for builder, tests in sorted(builders_and_tests.iteritems()):
497 print_text.append(' %s: %s' % (builder, tests))
498 parameters = {
499 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000500 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700501 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000502 'revision': options.revision,
503 }],
tandrii8c5a3532016-11-04 07:52:02 -0700504 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000505 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000506 if 'presubmit' in builder.lower():
507 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000508 if tests:
509 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700510
511 tags = [
512 'builder:%s' % builder,
513 'buildset:%s' % buildset,
514 'user_agent:git_cl_try',
515 ]
516 if master:
517 parameters['properties']['master'] = master
518 tags.append('master:%s' % master)
519
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000520 batch_req_body['builds'].append(
521 {
522 'bucket': bucket,
523 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000524 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700525 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000526 }
527 )
528
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000529 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700530 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000531 http,
532 buildbucket_put_url,
533 'PUT',
534 body=json.dumps(batch_req_body),
535 headers={'Content-Type': 'application/json'}
536 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000537 print_text.append('To see results here, run: git cl try-results')
538 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700539 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000540
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000541
tandrii221ab252016-10-06 08:12:04 -0700542def fetch_try_jobs(auth_config, changelist, buildbucket_host,
543 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700544 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000545
qyearsley53f48a12016-09-01 10:45:13 -0700546 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000547 """
tandrii221ab252016-10-06 08:12:04 -0700548 assert buildbucket_host
549 assert changelist.GetIssue(), 'CL must be uploaded first'
550 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
551 patchset = patchset or changelist.GetMostRecentPatchset()
552 assert patchset, 'CL must be uploaded first'
553
554 codereview_url = changelist.GetCodereviewServer()
555 codereview_host = urlparse.urlparse(codereview_url).hostname
556 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000557 if authenticator.has_cached_credentials():
558 http = authenticator.authorize(httplib2.Http())
559 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700560 print('Warning: Some results might be missing because %s' %
561 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700562 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000563 http = httplib2.Http()
564
565 http.force_exception_to_status_code = True
566
tandrii221ab252016-10-06 08:12:04 -0700567 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
568 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
569 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700571 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572 params = {'tag': 'buildset:%s' % buildset}
573
574 builds = {}
575 while True:
576 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700577 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000578 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700579 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000580 for build in content.get('builds', []):
581 builds[build['id']] = build
582 if 'next_cursor' in content:
583 params['start_cursor'] = content['next_cursor']
584 else:
585 break
586 return builds
587
588
qyearsleyeab3c042016-08-24 09:18:28 -0700589def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000590 """Prints nicely result of fetch_try_jobs."""
591 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700592 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000593 return
594
595 # Make a copy, because we'll be modifying builds dictionary.
596 builds = builds.copy()
597 builder_names_cache = {}
598
599 def get_builder(b):
600 try:
601 return builder_names_cache[b['id']]
602 except KeyError:
603 try:
604 parameters = json.loads(b['parameters_json'])
605 name = parameters['builder_name']
606 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700607 print('WARNING: failed to get builder name for build %s: %s' % (
608 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000609 name = None
610 builder_names_cache[b['id']] = name
611 return name
612
613 def get_bucket(b):
614 bucket = b['bucket']
615 if bucket.startswith('master.'):
616 return bucket[len('master.'):]
617 return bucket
618
619 if options.print_master:
620 name_fmt = '%%-%ds %%-%ds' % (
621 max(len(str(get_bucket(b))) for b in builds.itervalues()),
622 max(len(str(get_builder(b))) for b in builds.itervalues()))
623 def get_name(b):
624 return name_fmt % (get_bucket(b), get_builder(b))
625 else:
626 name_fmt = '%%-%ds' % (
627 max(len(str(get_builder(b))) for b in builds.itervalues()))
628 def get_name(b):
629 return name_fmt % get_builder(b)
630
631 def sort_key(b):
632 return b['status'], b.get('result'), get_name(b), b.get('url')
633
634 def pop(title, f, color=None, **kwargs):
635 """Pop matching builds from `builds` dict and print them."""
636
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000637 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000638 colorize = str
639 else:
640 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
641
642 result = []
643 for b in builds.values():
644 if all(b.get(k) == v for k, v in kwargs.iteritems()):
645 builds.pop(b['id'])
646 result.append(b)
647 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700648 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000649 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700650 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000651
652 total = len(builds)
653 pop(status='COMPLETED', result='SUCCESS',
654 title='Successes:', color=Fore.GREEN,
655 f=lambda b: (get_name(b), b.get('url')))
656 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
657 title='Infra Failures:', color=Fore.MAGENTA,
658 f=lambda b: (get_name(b), b.get('url')))
659 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
660 title='Failures:', color=Fore.RED,
661 f=lambda b: (get_name(b), b.get('url')))
662 pop(status='COMPLETED', result='CANCELED',
663 title='Canceled:', color=Fore.MAGENTA,
664 f=lambda b: (get_name(b),))
665 pop(status='COMPLETED', result='FAILURE',
666 failure_reason='INVALID_BUILD_DEFINITION',
667 title='Wrong master/builder name:', color=Fore.MAGENTA,
668 f=lambda b: (get_name(b),))
669 pop(status='COMPLETED', result='FAILURE',
670 title='Other failures:',
671 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
672 pop(status='COMPLETED',
673 title='Other finished:',
674 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
675 pop(status='STARTED',
676 title='Started:', color=Fore.YELLOW,
677 f=lambda b: (get_name(b), b.get('url')))
678 pop(status='SCHEDULED',
679 title='Scheduled:',
680 f=lambda b: (get_name(b), 'id=%s' % b['id']))
681 # The last section is just in case buildbucket API changes OR there is a bug.
682 pop(title='Other:',
683 f=lambda b: (get_name(b), 'id=%s' % b['id']))
684 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700685 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000686
687
qyearsley53f48a12016-09-01 10:45:13 -0700688def write_try_results_json(output_file, builds):
689 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
690
691 The input |builds| dict is assumed to be generated by Buildbucket.
692 Buildbucket documentation: http://goo.gl/G0s101
693 """
694
695 def convert_build_dict(build):
696 return {
697 'buildbucket_id': build.get('id'),
698 'status': build.get('status'),
699 'result': build.get('result'),
700 'bucket': build.get('bucket'),
701 'builder_name': json.loads(
702 build.get('parameters_json', '{}')).get('builder_name'),
703 'failure_reason': build.get('failure_reason'),
704 'url': build.get('url'),
705 }
706
707 converted = []
708 for _, build in sorted(builds.items()):
709 converted.append(convert_build_dict(build))
710 write_json(output_file, converted)
711
712
iannucci@chromium.org79540052012-10-19 23:15:26 +0000713def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000714 """Prints statistics about the change to the user."""
715 # --no-ext-diff is broken in some versions of Git, so try to work around
716 # this by overriding the environment (but there is still a problem if the
717 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000718 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000719 if 'GIT_EXTERNAL_DIFF' in env:
720 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000721
722 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800723 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000724 else:
725 similarity_options = ['-M%s' % similarity]
726
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000727 try:
728 stdout = sys.stdout.fileno()
729 except AttributeError:
730 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000731 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000732 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000733 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000734 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000735
736
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000737class BuildbucketResponseException(Exception):
738 pass
739
740
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741class Settings(object):
742 def __init__(self):
743 self.default_server = None
744 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000745 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746 self.tree_status_url = None
747 self.viewvc_url = None
748 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000749 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000750 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000751 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000752 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000753 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000754 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755
756 def LazyUpdateIfNeeded(self):
757 """Updates the settings from a codereview.settings file, if available."""
758 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000759 # The only value that actually changes the behavior is
760 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000761 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000762 error_ok=True
763 ).strip().lower()
764
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000766 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 LoadCodereviewSettingsFromFile(cr_settings_file)
768 self.updated = True
769
770 def GetDefaultServerUrl(self, error_ok=False):
771 if not self.default_server:
772 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000773 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000774 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775 if error_ok:
776 return self.default_server
777 if not self.default_server:
778 error_message = ('Could not find settings file. You must configure '
779 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000780 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000781 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782 return self.default_server
783
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000784 @staticmethod
785 def GetRelativeRoot():
786 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000787
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000789 if self.root is None:
790 self.root = os.path.abspath(self.GetRelativeRoot())
791 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000793 def GetGitMirror(self, remote='origin'):
794 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000795 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000796 if not os.path.isdir(local_url):
797 return None
798 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
799 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100800 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100801 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000802 if mirror.exists():
803 return mirror
804 return None
805
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000806 def GetTreeStatusUrl(self, error_ok=False):
807 if not self.tree_status_url:
808 error_message = ('You must configure your tree status URL by running '
809 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000810 self.tree_status_url = self._GetRietveldConfig(
811 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 return self.tree_status_url
813
814 def GetViewVCUrl(self):
815 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000816 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000817 return self.viewvc_url
818
rmistry@google.com90752582014-01-14 21:04:50 +0000819 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000820 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000821
rmistry@google.com78948ed2015-07-08 23:09:57 +0000822 def GetIsSkipDependencyUpload(self, branch_name):
823 """Returns true if specified branch should skip dep uploads."""
824 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
825 error_ok=True)
826
rmistry@google.com5626a922015-02-26 14:03:30 +0000827 def GetRunPostUploadHook(self):
828 run_post_upload_hook = self._GetRietveldConfig(
829 'run-post-upload-hook', error_ok=True)
830 return run_post_upload_hook == "True"
831
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000832 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000833 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000834
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000835 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000837
ukai@chromium.orge8077812012-02-03 03:41:46 +0000838 def GetIsGerrit(self):
839 """Return true if this repo is assosiated with gerrit code review system."""
840 if self.is_gerrit is None:
841 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
842 return self.is_gerrit
843
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000844 def GetSquashGerritUploads(self):
845 """Return true if uploads to Gerrit should be squashed by default."""
846 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700847 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
848 if self.squash_gerrit_uploads is None:
849 # Default is squash now (http://crbug.com/611892#c23).
850 self.squash_gerrit_uploads = not (
851 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
852 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000853 return self.squash_gerrit_uploads
854
tandriia60502f2016-06-20 02:01:53 -0700855 def GetSquashGerritUploadsOverride(self):
856 """Return True or False if codereview.settings should be overridden.
857
858 Returns None if no override has been defined.
859 """
860 # See also http://crbug.com/611892#c23
861 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
862 error_ok=True).strip()
863 if result == 'true':
864 return True
865 if result == 'false':
866 return False
867 return None
868
tandrii@chromium.org28253532016-04-14 13:46:56 +0000869 def GetGerritSkipEnsureAuthenticated(self):
870 """Return True if EnsureAuthenticated should not be done for Gerrit
871 uploads."""
872 if self.gerrit_skip_ensure_authenticated is None:
873 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000874 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000875 error_ok=True).strip() == 'true')
876 return self.gerrit_skip_ensure_authenticated
877
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000878 def GetGitEditor(self):
879 """Return the editor specified in the git config, or None if none is."""
880 if self.git_editor is None:
881 self.git_editor = self._GetConfig('core.editor', error_ok=True)
882 return self.git_editor or None
883
thestig@chromium.org44202a22014-03-11 19:22:18 +0000884 def GetLintRegex(self):
885 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
886 DEFAULT_LINT_REGEX)
887
888 def GetLintIgnoreRegex(self):
889 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
890 DEFAULT_LINT_IGNORE_REGEX)
891
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000892 def GetProject(self):
893 if not self.project:
894 self.project = self._GetRietveldConfig('project', error_ok=True)
895 return self.project
896
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000897 def _GetRietveldConfig(self, param, **kwargs):
898 return self._GetConfig('rietveld.' + param, **kwargs)
899
rmistry@google.com78948ed2015-07-08 23:09:57 +0000900 def _GetBranchConfig(self, branch_name, param, **kwargs):
901 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
902
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000903 def _GetConfig(self, param, **kwargs):
904 self.LazyUpdateIfNeeded()
905 return RunGit(['config', param], **kwargs).strip()
906
907
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100908@contextlib.contextmanager
909def _get_gerrit_project_config_file(remote_url):
910 """Context manager to fetch and store Gerrit's project.config from
911 refs/meta/config branch and store it in temp file.
912
913 Provides a temporary filename or None if there was error.
914 """
915 error, _ = RunGitWithCode([
916 'fetch', remote_url,
917 '+refs/meta/config:refs/git_cl/meta/config'])
918 if error:
919 # Ref doesn't exist or isn't accessible to current user.
920 print('WARNING: failed to fetch project config for %s: %s' %
921 (remote_url, error))
922 yield None
923 return
924
925 error, project_config_data = RunGitWithCode(
926 ['show', 'refs/git_cl/meta/config:project.config'])
927 if error:
928 print('WARNING: project.config file not found')
929 yield None
930 return
931
932 with gclient_utils.temporary_directory() as tempdir:
933 project_config_file = os.path.join(tempdir, 'project.config')
934 gclient_utils.FileWrite(project_config_file, project_config_data)
935 yield project_config_file
936
937
938def _is_git_numberer_enabled(remote_url, remote_ref):
939 """Returns True if Git Numberer is enabled on this ref."""
940 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100941 KNOWN_PROJECTS_WHITELIST = [
942 'chromium/src',
943 'external/webrtc',
944 'v8/v8',
945 ]
946
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100947 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
948 url_parts = urlparse.urlparse(remote_url)
949 project_name = url_parts.path.lstrip('/').rstrip('git./')
950 for known in KNOWN_PROJECTS_WHITELIST:
951 if project_name.endswith(known):
952 break
953 else:
954 # Early exit to avoid extra fetches for repos that aren't using Git
955 # Numberer.
956 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100957
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100958 with _get_gerrit_project_config_file(remote_url) as project_config_file:
959 if project_config_file is None:
960 # Failed to fetch project.config, which shouldn't happen on open source
961 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100962 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100963 def get_opts(x):
964 code, out = RunGitWithCode(
965 ['config', '-f', project_config_file, '--get-all',
966 'plugin.git-numberer.validate-%s-refglob' % x])
967 if code == 0:
968 return out.strip().splitlines()
969 return []
970 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100971
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100972 logging.info('validator config enabled %s disabled %s refglobs for '
973 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000974
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100975 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100976 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100977 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100978 return True
979 return False
980
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100981 if match_refglobs(disabled):
982 return False
983 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100984
985
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000986def ShortBranchName(branch):
987 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000988 return branch.replace('refs/heads/', '', 1)
989
990
991def GetCurrentBranchRef():
992 """Returns branch ref (e.g., refs/heads/master) or None."""
993 return RunGit(['symbolic-ref', 'HEAD'],
994 stderr=subprocess2.VOID, error_ok=True).strip() or None
995
996
997def GetCurrentBranch():
998 """Returns current branch or None.
999
1000 For refs/heads/* branches, returns just last part. For others, full ref.
1001 """
1002 branchref = GetCurrentBranchRef()
1003 if branchref:
1004 return ShortBranchName(branchref)
1005 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006
1007
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001008class _CQState(object):
1009 """Enum for states of CL with respect to Commit Queue."""
1010 NONE = 'none'
1011 DRY_RUN = 'dry_run'
1012 COMMIT = 'commit'
1013
1014 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1015
1016
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001017class _ParsedIssueNumberArgument(object):
1018 def __init__(self, issue=None, patchset=None, hostname=None):
1019 self.issue = issue
1020 self.patchset = patchset
1021 self.hostname = hostname
1022
1023 @property
1024 def valid(self):
1025 return self.issue is not None
1026
1027
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001028def ParseIssueNumberArgument(arg):
1029 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1030 fail_result = _ParsedIssueNumberArgument()
1031
1032 if arg.isdigit():
1033 return _ParsedIssueNumberArgument(issue=int(arg))
1034 if not arg.startswith('http'):
1035 return fail_result
1036 url = gclient_utils.UpgradeToHttps(arg)
1037 try:
1038 parsed_url = urlparse.urlparse(url)
1039 except ValueError:
1040 return fail_result
1041 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1042 tmp = cls.ParseIssueURL(parsed_url)
1043 if tmp is not None:
1044 return tmp
1045 return fail_result
1046
1047
Aaron Gablea45ee112016-11-22 15:14:38 -08001048class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001049 def __init__(self, issue, url):
1050 self.issue = issue
1051 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001052 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001053
1054 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001055 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001056 self.issue, self.url)
1057
1058
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001060 """Changelist works with one changelist in local branch.
1061
1062 Supports two codereview backends: Rietveld or Gerrit, selected at object
1063 creation.
1064
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001065 Notes:
1066 * Not safe for concurrent multi-{thread,process} use.
1067 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001068 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001069 """
1070
1071 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1072 """Create a new ChangeList instance.
1073
1074 If issue is given, the codereview must be given too.
1075
1076 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1077 Otherwise, it's decided based on current configuration of the local branch,
1078 with default being 'rietveld' for backwards compatibility.
1079 See _load_codereview_impl for more details.
1080
1081 **kwargs will be passed directly to codereview implementation.
1082 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001084 global settings
1085 if not settings:
1086 # Happens when git_cl.py is used as a utility library.
1087 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001088
1089 if issue:
1090 assert codereview, 'codereview must be known, if issue is known'
1091
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092 self.branchref = branchref
1093 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001094 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 self.branch = ShortBranchName(self.branchref)
1096 else:
1097 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001099 self.lookedup_issue = False
1100 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101 self.has_description = False
1102 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001103 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001105 self.cc = None
1106 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001107 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001108
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001109 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001110 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001111 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001112 assert self._codereview_impl
1113 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001114
1115 def _load_codereview_impl(self, codereview=None, **kwargs):
1116 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001117 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1118 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1119 self._codereview = codereview
1120 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001121 return
1122
1123 # Automatic selection based on issue number set for a current branch.
1124 # Rietveld takes precedence over Gerrit.
1125 assert not self.issue
1126 # Whether we find issue or not, we are doing the lookup.
1127 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001128 if self.GetBranch():
1129 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1130 issue = _git_get_branch_config_value(
1131 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1132 if issue:
1133 self._codereview = codereview
1134 self._codereview_impl = cls(self, **kwargs)
1135 self.issue = int(issue)
1136 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001137
1138 # No issue is set for this branch, so decide based on repo-wide settings.
1139 return self._load_codereview_impl(
1140 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1141 **kwargs)
1142
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001143 def IsGerrit(self):
1144 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001145
1146 def GetCCList(self):
1147 """Return the users cc'd on this CL.
1148
agable92bec4f2016-08-24 09:27:27 -07001149 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001150 """
1151 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001152 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153 more_cc = ','.join(self.watchers)
1154 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1155 return self.cc
1156
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001157 def GetCCListWithoutDefault(self):
1158 """Return the users cc'd on this CL excluding default ones."""
1159 if self.cc is None:
1160 self.cc = ','.join(self.watchers)
1161 return self.cc
1162
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001163 def SetWatchers(self, watchers):
1164 """Set the list of email addresses that should be cc'd based on the changed
1165 files in this CL.
1166 """
1167 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168
1169 def GetBranch(self):
1170 """Returns the short branch name, e.g. 'master'."""
1171 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001172 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001173 if not branchref:
1174 return None
1175 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 self.branch = ShortBranchName(self.branchref)
1177 return self.branch
1178
1179 def GetBranchRef(self):
1180 """Returns the full branch name, e.g. 'refs/heads/master'."""
1181 self.GetBranch() # Poke the lazy loader.
1182 return self.branchref
1183
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001184 def ClearBranch(self):
1185 """Clears cached branch data of this object."""
1186 self.branch = self.branchref = None
1187
tandrii5d48c322016-08-18 16:19:37 -07001188 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1189 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1190 kwargs['branch'] = self.GetBranch()
1191 return _git_get_branch_config_value(key, default, **kwargs)
1192
1193 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1194 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1195 assert self.GetBranch(), (
1196 'this CL must have an associated branch to %sset %s%s' %
1197 ('un' if value is None else '',
1198 key,
1199 '' if value is None else ' to %r' % value))
1200 kwargs['branch'] = self.GetBranch()
1201 return _git_set_branch_config_value(key, value, **kwargs)
1202
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001203 @staticmethod
1204 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001205 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 e.g. 'origin', 'refs/heads/master'
1207 """
1208 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001209 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1210
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001212 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001214 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1215 error_ok=True).strip()
1216 if upstream_branch:
1217 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001219 # Else, try to guess the origin remote.
1220 remote_branches = RunGit(['branch', '-r']).split()
1221 if 'origin/master' in remote_branches:
1222 # Fall back on origin/master if it exits.
1223 remote = 'origin'
1224 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001226 DieWithError(
1227 'Unable to determine default branch to diff against.\n'
1228 'Either pass complete "git diff"-style arguments, like\n'
1229 ' git cl upload origin/master\n'
1230 'or verify this branch is set up to track another \n'
1231 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232
1233 return remote, upstream_branch
1234
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001235 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001236 upstream_branch = self.GetUpstreamBranch()
1237 if not BranchExists(upstream_branch):
1238 DieWithError('The upstream for the current branch (%s) does not exist '
1239 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001240 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001241 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243 def GetUpstreamBranch(self):
1244 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001245 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001247 upstream_branch = upstream_branch.replace('refs/heads/',
1248 'refs/remotes/%s/' % remote)
1249 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1250 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 self.upstream_branch = upstream_branch
1252 return self.upstream_branch
1253
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001254 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001255 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001256 remote, branch = None, self.GetBranch()
1257 seen_branches = set()
1258 while branch not in seen_branches:
1259 seen_branches.add(branch)
1260 remote, branch = self.FetchUpstreamTuple(branch)
1261 branch = ShortBranchName(branch)
1262 if remote != '.' or branch.startswith('refs/remotes'):
1263 break
1264 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001265 remotes = RunGit(['remote'], error_ok=True).split()
1266 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001267 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001268 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001269 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001270 logging.warn('Could not determine which remote this change is '
1271 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001272 else:
1273 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001274 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001275 branch = 'HEAD'
1276 if branch.startswith('refs/remotes'):
1277 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001278 elif branch.startswith('refs/branch-heads/'):
1279 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001280 else:
1281 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001282 return self._remote
1283
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001284 def GitSanityChecks(self, upstream_git_obj):
1285 """Checks git repo status and ensures diff is from local commits."""
1286
sbc@chromium.org79706062015-01-14 21:18:12 +00001287 if upstream_git_obj is None:
1288 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001289 print('ERROR: unable to determine current branch (detached HEAD?)',
1290 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001291 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001292 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001293 return False
1294
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001295 # Verify the commit we're diffing against is in our current branch.
1296 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1297 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1298 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001299 print('ERROR: %s is not in the current branch. You may need to rebase '
1300 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 return False
1302
1303 # List the commits inside the diff, and verify they are all local.
1304 commits_in_diff = RunGit(
1305 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1306 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1307 remote_branch = remote_branch.strip()
1308 if code != 0:
1309 _, remote_branch = self.GetRemoteBranch()
1310
1311 commits_in_remote = RunGit(
1312 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1313
1314 common_commits = set(commits_in_diff) & set(commits_in_remote)
1315 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001316 print('ERROR: Your diff contains %d commits already in %s.\n'
1317 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1318 'the diff. If you are using a custom git flow, you can override'
1319 ' the reference used for this check with "git config '
1320 'gitcl.remotebranch <git-ref>".' % (
1321 len(common_commits), remote_branch, upstream_git_obj),
1322 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001323 return False
1324 return True
1325
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001326 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001327 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001328
1329 Returns None if it is not set.
1330 """
tandrii5d48c322016-08-18 16:19:37 -07001331 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001332
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001333 def GetRemoteUrl(self):
1334 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1335
1336 Returns None if there is no remote.
1337 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001338 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001339 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1340
1341 # If URL is pointing to a local directory, it is probably a git cache.
1342 if os.path.isdir(url):
1343 url = RunGit(['config', 'remote.%s.url' % remote],
1344 error_ok=True,
1345 cwd=url).strip()
1346 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001348 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001349 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001350 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001351 self.issue = self._GitGetBranchConfigValue(
1352 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001353 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354 return self.issue
1355
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 def GetIssueURL(self):
1357 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001358 issue = self.GetIssue()
1359 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001360 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001361 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362
1363 def GetDescription(self, pretty=False):
1364 if not self.has_description:
1365 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001366 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367 self.has_description = True
1368 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001369 # Set width to 72 columns + 2 space indent.
1370 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001372 lines = self.description.splitlines()
1373 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 return self.description
1375
1376 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001377 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001378 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001379 self.patchset = self._GitGetBranchConfigValue(
1380 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001381 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382 return self.patchset
1383
1384 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001385 """Set this branch's patchset. If patchset=0, clears the patchset."""
1386 assert self.GetBranch()
1387 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001388 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001389 else:
1390 self.patchset = int(patchset)
1391 self._GitSetBranchConfigValue(
1392 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001394 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001395 """Set this branch's issue. If issue isn't given, clears the issue."""
1396 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001398 issue = int(issue)
1399 self._GitSetBranchConfigValue(
1400 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001401 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001402 codereview_server = self._codereview_impl.GetCodereviewServer()
1403 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001404 self._GitSetBranchConfigValue(
1405 self._codereview_impl.CodereviewServerConfigKey(),
1406 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407 else:
tandrii5d48c322016-08-18 16:19:37 -07001408 # Reset all of these just to be clean.
1409 reset_suffixes = [
1410 'last-upload-hash',
1411 self._codereview_impl.IssueConfigKey(),
1412 self._codereview_impl.PatchsetConfigKey(),
1413 self._codereview_impl.CodereviewServerConfigKey(),
1414 ] + self._PostUnsetIssueProperties()
1415 for prop in reset_suffixes:
1416 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001417 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001418 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419
dnjba1b0f32016-09-02 12:37:42 -07001420 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001421 if not self.GitSanityChecks(upstream_branch):
1422 DieWithError('\nGit sanity check failure')
1423
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001424 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001425 if not root:
1426 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001427 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001428
1429 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001430 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001431 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001432 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001433 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001434 except subprocess2.CalledProcessError:
1435 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001436 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001437 'This branch probably doesn\'t exist anymore. To reset the\n'
1438 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001439 ' git branch --set-upstream-to origin/master %s\n'
1440 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001441 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001442
maruel@chromium.org52424302012-08-29 15:14:30 +00001443 issue = self.GetIssue()
1444 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001445 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001446 description = self.GetDescription()
1447 else:
1448 # If the change was never uploaded, use the log messages of all commits
1449 # up to the branch point, as git cl upload will prefill the description
1450 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001451 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1452 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001453
1454 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001455 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001456 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001457 name,
1458 description,
1459 absroot,
1460 files,
1461 issue,
1462 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001463 author,
1464 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001465
dsansomee2d6fd92016-09-08 00:10:47 -07001466 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001467 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001468 return self._codereview_impl.UpdateDescriptionRemote(
1469 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001470
1471 def RunHook(self, committing, may_prompt, verbose, change):
1472 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1473 try:
1474 return presubmit_support.DoPresubmitChecks(change, committing,
1475 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1476 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001477 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1478 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001479 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001480 DieWithError(
1481 ('%s\nMaybe your depot_tools is out of date?\n'
1482 'If all fails, contact maruel@') % e)
1483
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001484 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1485 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001486 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1487 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001488 else:
1489 # Assume url.
1490 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1491 urlparse.urlparse(issue_arg))
1492 if not parsed_issue_arg or not parsed_issue_arg.valid:
1493 DieWithError('Failed to parse issue argument "%s". '
1494 'Must be an issue number or a valid URL.' % issue_arg)
1495 return self._codereview_impl.CMDPatchWithParsedIssue(
1496 parsed_issue_arg, reject, nocommit, directory)
1497
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001498 def CMDUpload(self, options, git_diff_args, orig_args):
1499 """Uploads a change to codereview."""
1500 if git_diff_args:
1501 # TODO(ukai): is it ok for gerrit case?
1502 base_branch = git_diff_args[0]
1503 else:
1504 if self.GetBranch() is None:
1505 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1506
1507 # Default to diffing against common ancestor of upstream branch
1508 base_branch = self.GetCommonAncestorWithUpstream()
1509 git_diff_args = [base_branch, 'HEAD']
1510
1511 # Make sure authenticated to codereview before running potentially expensive
1512 # hooks. It is a fast, best efforts check. Codereview still can reject the
1513 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001514 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001515
1516 # Apply watchlists on upload.
1517 change = self.GetChange(base_branch, None)
1518 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1519 files = [f.LocalPath() for f in change.AffectedFiles()]
1520 if not options.bypass_watchlists:
1521 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1522
1523 if not options.bypass_hooks:
1524 if options.reviewers or options.tbr_owners:
1525 # Set the reviewer list now so that presubmit checks can access it.
1526 change_description = ChangeDescription(change.FullDescriptionText())
1527 change_description.update_reviewers(options.reviewers,
1528 options.tbr_owners,
1529 change)
1530 change.SetDescriptionText(change_description.description)
1531 hook_results = self.RunHook(committing=False,
1532 may_prompt=not options.force,
1533 verbose=options.verbose,
1534 change=change)
1535 if not hook_results.should_continue():
1536 return 1
1537 if not options.reviewers and hook_results.reviewers:
1538 options.reviewers = hook_results.reviewers.split(',')
1539
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001540 # TODO(tandrii): Checking local patchset against remote patchset is only
1541 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1542 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001543 latest_patchset = self.GetMostRecentPatchset()
1544 local_patchset = self.GetPatchset()
1545 if (latest_patchset and local_patchset and
1546 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001547 print('The last upload made from this repository was patchset #%d but '
1548 'the most recent patchset on the server is #%d.'
1549 % (local_patchset, latest_patchset))
1550 print('Uploading will still work, but if you\'ve uploaded to this '
1551 'issue from another machine or branch the patch you\'re '
1552 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001553 ask_for_data('About to upload; enter to confirm.')
1554
1555 print_stats(options.similarity, options.find_copies, git_diff_args)
1556 ret = self.CMDUploadChange(options, git_diff_args, change)
1557 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001558 if options.use_commit_queue:
1559 self.SetCQState(_CQState.COMMIT)
1560 elif options.cq_dry_run:
1561 self.SetCQState(_CQState.DRY_RUN)
1562
tandrii5d48c322016-08-18 16:19:37 -07001563 _git_set_branch_config_value('last-upload-hash',
1564 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001565 # Run post upload hooks, if specified.
1566 if settings.GetRunPostUploadHook():
1567 presubmit_support.DoPostUploadExecuter(
1568 change,
1569 self,
1570 settings.GetRoot(),
1571 options.verbose,
1572 sys.stdout)
1573
1574 # Upload all dependencies if specified.
1575 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001576 print()
1577 print('--dependencies has been specified.')
1578 print('All dependent local branches will be re-uploaded.')
1579 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001580 # Remove the dependencies flag from args so that we do not end up in a
1581 # loop.
1582 orig_args.remove('--dependencies')
1583 ret = upload_branch_deps(self, orig_args)
1584 return ret
1585
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001586 def SetCQState(self, new_state):
1587 """Update the CQ state for latest patchset.
1588
1589 Issue must have been already uploaded and known.
1590 """
1591 assert new_state in _CQState.ALL_STATES
1592 assert self.GetIssue()
1593 return self._codereview_impl.SetCQState(new_state)
1594
qyearsley1fdfcb62016-10-24 13:22:03 -07001595 def TriggerDryRun(self):
1596 """Triggers a dry run and prints a warning on failure."""
1597 # TODO(qyearsley): Either re-use this method in CMDset_commit
1598 # and CMDupload, or change CMDtry to trigger dry runs with
1599 # just SetCQState, and catch keyboard interrupt and other
1600 # errors in that method.
1601 try:
1602 self.SetCQState(_CQState.DRY_RUN)
1603 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1604 return 0
1605 except KeyboardInterrupt:
1606 raise
1607 except:
1608 print('WARNING: failed to trigger CQ Dry Run.\n'
1609 'Either:\n'
1610 ' * your project has no CQ\n'
1611 ' * you don\'t have permission to trigger Dry Run\n'
1612 ' * bug in this code (see stack trace below).\n'
1613 'Consider specifying which bots to trigger manually '
1614 'or asking your project owners for permissions '
1615 'or contacting Chrome Infrastructure team at '
1616 'https://www.chromium.org/infra\n\n')
1617 # Still raise exception so that stack trace is printed.
1618 raise
1619
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001620 # Forward methods to codereview specific implementation.
1621
1622 def CloseIssue(self):
1623 return self._codereview_impl.CloseIssue()
1624
1625 def GetStatus(self):
1626 return self._codereview_impl.GetStatus()
1627
1628 def GetCodereviewServer(self):
1629 return self._codereview_impl.GetCodereviewServer()
1630
tandriide281ae2016-10-12 06:02:30 -07001631 def GetIssueOwner(self):
1632 """Get owner from codereview, which may differ from this checkout."""
1633 return self._codereview_impl.GetIssueOwner()
1634
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001635 def GetApprovingReviewers(self):
1636 return self._codereview_impl.GetApprovingReviewers()
1637
1638 def GetMostRecentPatchset(self):
1639 return self._codereview_impl.GetMostRecentPatchset()
1640
tandriide281ae2016-10-12 06:02:30 -07001641 def CannotTriggerTryJobReason(self):
1642 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1643 return self._codereview_impl.CannotTriggerTryJobReason()
1644
tandrii8c5a3532016-11-04 07:52:02 -07001645 def GetTryjobProperties(self, patchset=None):
1646 """Returns dictionary of properties to launch tryjob."""
1647 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1648
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001649 def __getattr__(self, attr):
1650 # This is because lots of untested code accesses Rietveld-specific stuff
1651 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001652 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001653 # Note that child method defines __getattr__ as well, and forwards it here,
1654 # because _RietveldChangelistImpl is not cleaned up yet, and given
1655 # deprecation of Rietveld, it should probably be just removed.
1656 # Until that time, avoid infinite recursion by bypassing __getattr__
1657 # of implementation class.
1658 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001659
1660
1661class _ChangelistCodereviewBase(object):
1662 """Abstract base class encapsulating codereview specifics of a changelist."""
1663 def __init__(self, changelist):
1664 self._changelist = changelist # instance of Changelist
1665
1666 def __getattr__(self, attr):
1667 # Forward methods to changelist.
1668 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1669 # _RietveldChangelistImpl to avoid this hack?
1670 return getattr(self._changelist, attr)
1671
1672 def GetStatus(self):
1673 """Apply a rough heuristic to give a simple summary of an issue's review
1674 or CQ status, assuming adherence to a common workflow.
1675
1676 Returns None if no issue for this branch, or specific string keywords.
1677 """
1678 raise NotImplementedError()
1679
1680 def GetCodereviewServer(self):
1681 """Returns server URL without end slash, like "https://codereview.com"."""
1682 raise NotImplementedError()
1683
1684 def FetchDescription(self):
1685 """Fetches and returns description from the codereview server."""
1686 raise NotImplementedError()
1687
tandrii5d48c322016-08-18 16:19:37 -07001688 @classmethod
1689 def IssueConfigKey(cls):
1690 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001691 raise NotImplementedError()
1692
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001693 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001694 def PatchsetConfigKey(cls):
1695 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001696 raise NotImplementedError()
1697
tandrii5d48c322016-08-18 16:19:37 -07001698 @classmethod
1699 def CodereviewServerConfigKey(cls):
1700 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001701 raise NotImplementedError()
1702
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001703 def _PostUnsetIssueProperties(self):
1704 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001705 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001706
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001707 def GetRieveldObjForPresubmit(self):
1708 # This is an unfortunate Rietveld-embeddedness in presubmit.
1709 # For non-Rietveld codereviews, this probably should return a dummy object.
1710 raise NotImplementedError()
1711
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001712 def GetGerritObjForPresubmit(self):
1713 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1714 return None
1715
dsansomee2d6fd92016-09-08 00:10:47 -07001716 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001717 """Update the description on codereview site."""
1718 raise NotImplementedError()
1719
1720 def CloseIssue(self):
1721 """Closes the issue."""
1722 raise NotImplementedError()
1723
1724 def GetApprovingReviewers(self):
1725 """Returns a list of reviewers approving the change.
1726
1727 Note: not necessarily committers.
1728 """
1729 raise NotImplementedError()
1730
1731 def GetMostRecentPatchset(self):
1732 """Returns the most recent patchset number from the codereview site."""
1733 raise NotImplementedError()
1734
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001735 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1736 directory):
1737 """Fetches and applies the issue.
1738
1739 Arguments:
1740 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1741 reject: if True, reject the failed patch instead of switching to 3-way
1742 merge. Rietveld only.
1743 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1744 only.
1745 directory: switch to directory before applying the patch. Rietveld only.
1746 """
1747 raise NotImplementedError()
1748
1749 @staticmethod
1750 def ParseIssueURL(parsed_url):
1751 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1752 failed."""
1753 raise NotImplementedError()
1754
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001755 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001756 """Best effort check that user is authenticated with codereview server.
1757
1758 Arguments:
1759 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001760 refresh: whether to attempt to refresh credentials. Ignored if not
1761 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001762 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001763 raise NotImplementedError()
1764
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001765 def CMDUploadChange(self, options, args, change):
1766 """Uploads a change to codereview."""
1767 raise NotImplementedError()
1768
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001769 def SetCQState(self, new_state):
1770 """Update the CQ state for latest patchset.
1771
1772 Issue must have been already uploaded and known.
1773 """
1774 raise NotImplementedError()
1775
tandriie113dfd2016-10-11 10:20:12 -07001776 def CannotTriggerTryJobReason(self):
1777 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1778 raise NotImplementedError()
1779
tandriide281ae2016-10-12 06:02:30 -07001780 def GetIssueOwner(self):
1781 raise NotImplementedError()
1782
tandrii8c5a3532016-11-04 07:52:02 -07001783 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001784 raise NotImplementedError()
1785
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001786
1787class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001788 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001789 super(_RietveldChangelistImpl, self).__init__(changelist)
1790 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001791 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001792 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001793
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001794 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001795 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001796 self._props = None
1797 self._rpc_server = None
1798
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001799 def GetCodereviewServer(self):
1800 if not self._rietveld_server:
1801 # If we're on a branch then get the server potentially associated
1802 # with that branch.
1803 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001804 self._rietveld_server = gclient_utils.UpgradeToHttps(
1805 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001806 if not self._rietveld_server:
1807 self._rietveld_server = settings.GetDefaultServerUrl()
1808 return self._rietveld_server
1809
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001810 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001811 """Best effort check that user is authenticated with Rietveld server."""
1812 if self._auth_config.use_oauth2:
1813 authenticator = auth.get_authenticator_for_host(
1814 self.GetCodereviewServer(), self._auth_config)
1815 if not authenticator.has_cached_credentials():
1816 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001817 if refresh:
1818 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001819
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001820 def FetchDescription(self):
1821 issue = self.GetIssue()
1822 assert issue
1823 try:
1824 return self.RpcServer().get_description(issue).strip()
1825 except urllib2.HTTPError as e:
1826 if e.code == 404:
1827 DieWithError(
1828 ('\nWhile fetching the description for issue %d, received a '
1829 '404 (not found)\n'
1830 'error. It is likely that you deleted this '
1831 'issue on the server. If this is the\n'
1832 'case, please run\n\n'
1833 ' git cl issue 0\n\n'
1834 'to clear the association with the deleted issue. Then run '
1835 'this command again.') % issue)
1836 else:
1837 DieWithError(
1838 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1839 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001840 print('Warning: Failed to retrieve CL description due to network '
1841 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842 return ''
1843
1844 def GetMostRecentPatchset(self):
1845 return self.GetIssueProperties()['patchsets'][-1]
1846
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001847 def GetIssueProperties(self):
1848 if self._props is None:
1849 issue = self.GetIssue()
1850 if not issue:
1851 self._props = {}
1852 else:
1853 self._props = self.RpcServer().get_issue_properties(issue, True)
1854 return self._props
1855
tandriie113dfd2016-10-11 10:20:12 -07001856 def CannotTriggerTryJobReason(self):
1857 props = self.GetIssueProperties()
1858 if not props:
1859 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1860 if props.get('closed'):
1861 return 'CL %s is closed' % self.GetIssue()
1862 if props.get('private'):
1863 return 'CL %s is private' % self.GetIssue()
1864 return None
1865
tandrii8c5a3532016-11-04 07:52:02 -07001866 def GetTryjobProperties(self, patchset=None):
1867 """Returns dictionary of properties to launch tryjob."""
1868 project = (self.GetIssueProperties() or {}).get('project')
1869 return {
1870 'issue': self.GetIssue(),
1871 'patch_project': project,
1872 'patch_storage': 'rietveld',
1873 'patchset': patchset or self.GetPatchset(),
1874 'rietveld': self.GetCodereviewServer(),
1875 }
1876
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001877 def GetApprovingReviewers(self):
1878 return get_approving_reviewers(self.GetIssueProperties())
1879
tandriide281ae2016-10-12 06:02:30 -07001880 def GetIssueOwner(self):
1881 return (self.GetIssueProperties() or {}).get('owner_email')
1882
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001883 def AddComment(self, message):
1884 return self.RpcServer().add_comment(self.GetIssue(), message)
1885
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001886 def GetStatus(self):
1887 """Apply a rough heuristic to give a simple summary of an issue's review
1888 or CQ status, assuming adherence to a common workflow.
1889
1890 Returns None if no issue for this branch, or one of the following keywords:
1891 * 'error' - error from review tool (including deleted issues)
1892 * 'unsent' - not sent for review
1893 * 'waiting' - waiting for review
1894 * 'reply' - waiting for owner to reply to review
1895 * 'lgtm' - LGTM from at least one approved reviewer
1896 * 'commit' - in the commit queue
1897 * 'closed' - closed
1898 """
1899 if not self.GetIssue():
1900 return None
1901
1902 try:
1903 props = self.GetIssueProperties()
1904 except urllib2.HTTPError:
1905 return 'error'
1906
1907 if props.get('closed'):
1908 # Issue is closed.
1909 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001910 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001911 # Issue is in the commit queue.
1912 return 'commit'
1913
1914 try:
1915 reviewers = self.GetApprovingReviewers()
1916 except urllib2.HTTPError:
1917 return 'error'
1918
1919 if reviewers:
1920 # Was LGTM'ed.
1921 return 'lgtm'
1922
1923 messages = props.get('messages') or []
1924
tandrii9d2c7a32016-06-22 03:42:45 -07001925 # Skip CQ messages that don't require owner's action.
1926 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1927 if 'Dry run:' in messages[-1]['text']:
1928 messages.pop()
1929 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1930 # This message always follows prior messages from CQ,
1931 # so skip this too.
1932 messages.pop()
1933 else:
1934 # This is probably a CQ messages warranting user attention.
1935 break
1936
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001937 if not messages:
1938 # No message was sent.
1939 return 'unsent'
1940 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001941 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001942 return 'reply'
1943 return 'waiting'
1944
dsansomee2d6fd92016-09-08 00:10:47 -07001945 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001946 return self.RpcServer().update_description(
1947 self.GetIssue(), self.description)
1948
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001949 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001950 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001952 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001953 return self.SetFlags({flag: value})
1954
1955 def SetFlags(self, flags):
1956 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001957 """
phajdan.jr68598232016-08-10 03:28:28 -07001958 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001959 try:
tandrii4b233bd2016-07-06 03:50:29 -07001960 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001961 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001962 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001963 if e.code == 404:
1964 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1965 if e.code == 403:
1966 DieWithError(
1967 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001968 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001969 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001971 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001972 """Returns an upload.RpcServer() to access this review's rietveld instance.
1973 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001974 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001975 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001976 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001977 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001978 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001980 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001981 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001982 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001983
tandrii5d48c322016-08-18 16:19:37 -07001984 @classmethod
1985 def PatchsetConfigKey(cls):
1986 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987
tandrii5d48c322016-08-18 16:19:37 -07001988 @classmethod
1989 def CodereviewServerConfigKey(cls):
1990 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001991
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001992 def GetRieveldObjForPresubmit(self):
1993 return self.RpcServer()
1994
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001995 def SetCQState(self, new_state):
1996 props = self.GetIssueProperties()
1997 if props.get('private'):
1998 DieWithError('Cannot set-commit on private issue')
1999
2000 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002001 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002002 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002003 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002004 else:
tandrii4b233bd2016-07-06 03:50:29 -07002005 assert new_state == _CQState.DRY_RUN
2006 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002007
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002008 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2009 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002010 # PatchIssue should never be called with a dirty tree. It is up to the
2011 # caller to check this, but just in case we assert here since the
2012 # consequences of the caller not checking this could be dire.
2013 assert(not git_common.is_dirty_git_tree('apply'))
2014 assert(parsed_issue_arg.valid)
2015 self._changelist.issue = parsed_issue_arg.issue
2016 if parsed_issue_arg.hostname:
2017 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2018
skobes6468b902016-10-24 08:45:10 -07002019 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2020 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2021 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002022 try:
skobes6468b902016-10-24 08:45:10 -07002023 scm_obj.apply_patch(patchset_object)
2024 except Exception as e:
2025 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002026 return 1
2027
2028 # If we had an issue, commit the current state and register the issue.
2029 if not nocommit:
2030 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2031 'patch from issue %(i)s at patchset '
2032 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2033 % {'i': self.GetIssue(), 'p': patchset})])
2034 self.SetIssue(self.GetIssue())
2035 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002036 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002037 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002038 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002039 return 0
2040
2041 @staticmethod
2042 def ParseIssueURL(parsed_url):
2043 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2044 return None
wychen3c1c1722016-08-04 11:46:36 -07002045 # Rietveld patch: https://domain/<number>/#ps<patchset>
2046 match = re.match(r'/(\d+)/$', parsed_url.path)
2047 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2048 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002049 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002050 issue=int(match.group(1)),
2051 patchset=int(match2.group(1)),
2052 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002053 # Typical url: https://domain/<issue_number>[/[other]]
2054 match = re.match('/(\d+)(/.*)?$', 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 hostname=parsed_url.netloc)
2059 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2060 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2061 if match:
skobes6468b902016-10-24 08:45:10 -07002062 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002063 issue=int(match.group(1)),
2064 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002065 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002066 return None
2067
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002068 def CMDUploadChange(self, options, args, change):
2069 """Upload the patch to Rietveld."""
2070 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2071 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002072 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2073 if options.emulate_svn_auto_props:
2074 upload_args.append('--emulate_svn_auto_props')
2075
2076 change_desc = None
2077
2078 if options.email is not None:
2079 upload_args.extend(['--email', options.email])
2080
2081 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002082 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002083 upload_args.extend(['--title', options.title])
2084 if options.message:
2085 upload_args.extend(['--message', options.message])
2086 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002087 print('This branch is associated with issue %s. '
2088 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002089 else:
nodirca166002016-06-27 10:59:51 -07002090 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002091 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002092 if options.message:
2093 message = options.message
2094 else:
2095 message = CreateDescriptionFromLog(args)
2096 if options.title:
2097 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002098 change_desc = ChangeDescription(message)
2099 if options.reviewers or options.tbr_owners:
2100 change_desc.update_reviewers(options.reviewers,
2101 options.tbr_owners,
2102 change)
2103 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002104 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002105
2106 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002107 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002108 return 1
2109
2110 upload_args.extend(['--message', change_desc.description])
2111 if change_desc.get_reviewers():
2112 upload_args.append('--reviewers=%s' % ','.join(
2113 change_desc.get_reviewers()))
2114 if options.send_mail:
2115 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002116 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002117 upload_args.append('--send_mail')
2118
2119 # We check this before applying rietveld.private assuming that in
2120 # rietveld.cc only addresses which we can send private CLs to are listed
2121 # if rietveld.private is set, and so we should ignore rietveld.cc only
2122 # when --private is specified explicitly on the command line.
2123 if options.private:
2124 logging.warn('rietveld.cc is ignored since private flag is specified. '
2125 'You need to review and add them manually if necessary.')
2126 cc = self.GetCCListWithoutDefault()
2127 else:
2128 cc = self.GetCCList()
2129 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002130 if change_desc.get_cced():
2131 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002132 if cc:
2133 upload_args.extend(['--cc', cc])
2134
2135 if options.private or settings.GetDefaultPrivateFlag() == "True":
2136 upload_args.append('--private')
2137
2138 upload_args.extend(['--git_similarity', str(options.similarity)])
2139 if not options.find_copies:
2140 upload_args.extend(['--git_no_find_copies'])
2141
2142 # Include the upstream repo's URL in the change -- this is useful for
2143 # projects that have their source spread across multiple repos.
2144 remote_url = self.GetGitBaseUrlFromConfig()
2145 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002146 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2147 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2148 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002149 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002150 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002151 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002152 if target_ref:
2153 upload_args.extend(['--target_ref', target_ref])
2154
2155 # Look for dependent patchsets. See crbug.com/480453 for more details.
2156 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2157 upstream_branch = ShortBranchName(upstream_branch)
2158 if remote is '.':
2159 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002160 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002161 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002162 print()
2163 print('Skipping dependency patchset upload because git config '
2164 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2165 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002166 else:
2167 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002168 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002169 auth_config=auth_config)
2170 branch_cl_issue_url = branch_cl.GetIssueURL()
2171 branch_cl_issue = branch_cl.GetIssue()
2172 branch_cl_patchset = branch_cl.GetPatchset()
2173 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2174 upload_args.extend(
2175 ['--depends_on_patchset', '%s:%s' % (
2176 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002177 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002178 '\n'
2179 'The current branch (%s) is tracking a local branch (%s) with '
2180 'an associated CL.\n'
2181 'Adding %s/#ps%s as a dependency patchset.\n'
2182 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2183 branch_cl_patchset))
2184
2185 project = settings.GetProject()
2186 if project:
2187 upload_args.extend(['--project', project])
2188
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002189 try:
2190 upload_args = ['upload'] + upload_args + args
2191 logging.info('upload.RealMain(%s)', upload_args)
2192 issue, patchset = upload.RealMain(upload_args)
2193 issue = int(issue)
2194 patchset = int(patchset)
2195 except KeyboardInterrupt:
2196 sys.exit(1)
2197 except:
2198 # If we got an exception after the user typed a description for their
2199 # change, back up the description before re-raising.
2200 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002201 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002202 raise
2203
2204 if not self.GetIssue():
2205 self.SetIssue(issue)
2206 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002207 return 0
2208
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002209
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002210class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002211 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002212 # auth_config is Rietveld thing, kept here to preserve interface only.
2213 super(_GerritChangelistImpl, self).__init__(changelist)
2214 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002215 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002216 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002217 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002218 # Map from change number (issue) to its detail cache.
2219 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002220
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002221 if codereview_host is not None:
2222 assert not codereview_host.startswith('https://'), codereview_host
2223 self._gerrit_host = codereview_host
2224 self._gerrit_server = 'https://%s' % codereview_host
2225
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002226 def _GetGerritHost(self):
2227 # Lazy load of configs.
2228 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002229 if self._gerrit_host and '.' not in self._gerrit_host:
2230 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2231 # This happens for internal stuff http://crbug.com/614312.
2232 parsed = urlparse.urlparse(self.GetRemoteUrl())
2233 if parsed.scheme == 'sso':
2234 print('WARNING: using non https URLs for remote is likely broken\n'
2235 ' Your current remote is: %s' % self.GetRemoteUrl())
2236 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2237 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002238 return self._gerrit_host
2239
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002240 def _GetGitHost(self):
2241 """Returns git host to be used when uploading change to Gerrit."""
2242 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2243
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002244 def GetCodereviewServer(self):
2245 if not self._gerrit_server:
2246 # If we're on a branch then get the server potentially associated
2247 # with that branch.
2248 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002249 self._gerrit_server = self._GitGetBranchConfigValue(
2250 self.CodereviewServerConfigKey())
2251 if self._gerrit_server:
2252 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002253 if not self._gerrit_server:
2254 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2255 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002256 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002257 parts[0] = parts[0] + '-review'
2258 self._gerrit_host = '.'.join(parts)
2259 self._gerrit_server = 'https://%s' % self._gerrit_host
2260 return self._gerrit_server
2261
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002262 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002263 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002264 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002265
tandrii5d48c322016-08-18 16:19:37 -07002266 @classmethod
2267 def PatchsetConfigKey(cls):
2268 return 'gerritpatchset'
2269
2270 @classmethod
2271 def CodereviewServerConfigKey(cls):
2272 return 'gerritserver'
2273
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002274 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002275 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002276 if settings.GetGerritSkipEnsureAuthenticated():
2277 # For projects with unusual authentication schemes.
2278 # See http://crbug.com/603378.
2279 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002280 # Lazy-loader to identify Gerrit and Git hosts.
2281 if gerrit_util.GceAuthenticator.is_gce():
2282 return
2283 self.GetCodereviewServer()
2284 git_host = self._GetGitHost()
2285 assert self._gerrit_server and self._gerrit_host
2286 cookie_auth = gerrit_util.CookiesAuthenticator()
2287
2288 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2289 git_auth = cookie_auth.get_auth_header(git_host)
2290 if gerrit_auth and git_auth:
2291 if gerrit_auth == git_auth:
2292 return
2293 print((
2294 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2295 ' Check your %s or %s file for credentials of hosts:\n'
2296 ' %s\n'
2297 ' %s\n'
2298 ' %s') %
2299 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2300 git_host, self._gerrit_host,
2301 cookie_auth.get_new_password_message(git_host)))
2302 if not force:
2303 ask_for_data('If you know what you are doing, press Enter to continue, '
2304 'Ctrl+C to abort.')
2305 return
2306 else:
2307 missing = (
2308 [] if gerrit_auth else [self._gerrit_host] +
2309 [] if git_auth else [git_host])
2310 DieWithError('Credentials for the following hosts are required:\n'
2311 ' %s\n'
2312 'These are read from %s (or legacy %s)\n'
2313 '%s' % (
2314 '\n '.join(missing),
2315 cookie_auth.get_gitcookies_path(),
2316 cookie_auth.get_netrc_path(),
2317 cookie_auth.get_new_password_message(git_host)))
2318
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002319 def _PostUnsetIssueProperties(self):
2320 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002321 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002322
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002323 def GetRieveldObjForPresubmit(self):
2324 class ThisIsNotRietveldIssue(object):
2325 def __nonzero__(self):
2326 # This is a hack to make presubmit_support think that rietveld is not
2327 # defined, yet still ensure that calls directly result in a decent
2328 # exception message below.
2329 return False
2330
2331 def __getattr__(self, attr):
2332 print(
2333 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2334 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2335 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2336 'or use Rietveld for codereview.\n'
2337 'See also http://crbug.com/579160.' % attr)
2338 raise NotImplementedError()
2339 return ThisIsNotRietveldIssue()
2340
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002341 def GetGerritObjForPresubmit(self):
2342 return presubmit_support.GerritAccessor(self._GetGerritHost())
2343
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002344 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002345 """Apply a rough heuristic to give a simple summary of an issue's review
2346 or CQ status, assuming adherence to a common workflow.
2347
2348 Returns None if no issue for this branch, or one of the following keywords:
2349 * 'error' - error from review tool (including deleted issues)
2350 * 'unsent' - no reviewers added
2351 * 'waiting' - waiting for review
2352 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002353 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002354 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002355 * 'commit' - in the commit queue
2356 * 'closed' - abandoned
2357 """
2358 if not self.GetIssue():
2359 return None
2360
2361 try:
2362 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002363 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002364 return 'error'
2365
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002366 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002367 return 'closed'
2368
2369 cq_label = data['labels'].get('Commit-Queue', {})
2370 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002371 votes = cq_label.get('all', [])
2372 highest_vote = 0
2373 for v in votes:
2374 highest_vote = max(highest_vote, v.get('value', 0))
2375 vote_value = str(highest_vote)
2376 if vote_value != '0':
2377 # Add a '+' if the value is not 0 to match the values in the label.
2378 # The cq_label does not have negatives.
2379 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002380 vote_text = cq_label.get('values', {}).get(vote_value, '')
2381 if vote_text.lower() == 'commit':
2382 return 'commit'
2383
2384 lgtm_label = data['labels'].get('Code-Review', {})
2385 if lgtm_label:
2386 if 'rejected' in lgtm_label:
2387 return 'not lgtm'
2388 if 'approved' in lgtm_label:
2389 return 'lgtm'
2390
2391 if not data.get('reviewers', {}).get('REVIEWER', []):
2392 return 'unsent'
2393
2394 messages = data.get('messages', [])
2395 if messages:
2396 owner = data['owner'].get('_account_id')
2397 last_message_author = messages[-1].get('author', {}).get('_account_id')
2398 if owner != last_message_author:
2399 # Some reply from non-owner.
2400 return 'reply'
2401
2402 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002403
2404 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002405 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002406 return data['revisions'][data['current_revision']]['_number']
2407
2408 def FetchDescription(self):
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002409 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002410 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002411 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002412
dsansomee2d6fd92016-09-08 00:10:47 -07002413 def UpdateDescriptionRemote(self, description, force=False):
2414 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2415 if not force:
2416 ask_for_data(
2417 'The description cannot be modified while the issue has a pending '
2418 'unpublished edit. Either publish the edit in the Gerrit web UI '
2419 'or delete it.\n\n'
2420 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2421
2422 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2423 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002424 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002425 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002426
2427 def CloseIssue(self):
2428 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2429
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002430 def GetApprovingReviewers(self):
2431 """Returns a list of reviewers approving the change.
2432
2433 Note: not necessarily committers.
2434 """
2435 raise NotImplementedError()
2436
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002437 def SubmitIssue(self, wait_for_merge=True):
2438 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2439 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002440
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002441 def _GetChangeDetail(self, options=None, issue=None,
2442 no_cache=False):
2443 """Returns details of the issue by querying Gerrit and caching results.
2444
2445 If fresh data is needed, set no_cache=True which will clear cache and
2446 thus new data will be fetched from Gerrit.
2447 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002448 options = options or []
2449 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002450 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002451
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002452 # Optimization to avoid multiple RPCs:
2453 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2454 'CURRENT_COMMIT' not in options):
2455 options.append('CURRENT_COMMIT')
2456
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002457 # Normalize issue and options for consistent keys in cache.
2458 issue = str(issue)
2459 options = [o.upper() for o in options]
2460
2461 # Check in cache first unless no_cache is True.
2462 if no_cache:
2463 self._detail_cache.pop(issue, None)
2464 else:
2465 options_set = frozenset(options)
2466 for cached_options_set, data in self._detail_cache.get(issue, []):
2467 # Assumption: data fetched before with extra options is suitable
2468 # for return for a smaller set of options.
2469 # For example, if we cached data for
2470 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2471 # and request is for options=[CURRENT_REVISION],
2472 # THEN we can return prior cached data.
2473 if options_set.issubset(cached_options_set):
2474 return data
2475
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002476 try:
2477 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2478 options, ignore_404=False)
2479 except gerrit_util.GerritError as e:
2480 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002481 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002482 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002483
2484 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002485 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002486
agable32978d92016-11-01 12:55:02 -07002487 def _GetChangeCommit(self, issue=None):
2488 issue = issue or self.GetIssue()
2489 assert issue, 'issue is required to query Gerrit'
2490 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2491 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002492 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002493 return data
2494
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002495 def CMDLand(self, force, bypass_hooks, verbose):
2496 if git_common.is_dirty_git_tree('land'):
2497 return 1
tandriid60367b2016-06-22 05:25:12 -07002498 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2499 if u'Commit-Queue' in detail.get('labels', {}):
2500 if not force:
2501 ask_for_data('\nIt seems this repository has a Commit Queue, '
2502 'which can test and land changes for you. '
2503 'Are you sure you wish to bypass it?\n'
2504 'Press Enter to continue, Ctrl+C to abort.')
2505
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002506 differs = True
tandriic4344b52016-08-29 06:04:54 -07002507 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002508 # Note: git diff outputs nothing if there is no diff.
2509 if not last_upload or RunGit(['diff', last_upload]).strip():
2510 print('WARNING: some changes from local branch haven\'t been uploaded')
2511 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002512 if detail['current_revision'] == last_upload:
2513 differs = False
2514 else:
2515 print('WARNING: local branch contents differ from latest uploaded '
2516 'patchset')
2517 if differs:
2518 if not force:
2519 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002520 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2521 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002522 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2523 elif not bypass_hooks:
2524 hook_results = self.RunHook(
2525 committing=True,
2526 may_prompt=not force,
2527 verbose=verbose,
2528 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2529 if not hook_results.should_continue():
2530 return 1
2531
2532 self.SubmitIssue(wait_for_merge=True)
2533 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002534 links = self._GetChangeCommit().get('web_links', [])
2535 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002536 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002537 print('Landed as %s' % link.get('url'))
2538 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002539 return 0
2540
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002541 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2542 directory):
2543 assert not reject
2544 assert not nocommit
2545 assert not directory
2546 assert parsed_issue_arg.valid
2547
2548 self._changelist.issue = parsed_issue_arg.issue
2549
2550 if parsed_issue_arg.hostname:
2551 self._gerrit_host = parsed_issue_arg.hostname
2552 self._gerrit_server = 'https://%s' % self._gerrit_host
2553
tandriic2405f52016-10-10 08:13:15 -07002554 try:
2555 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002556 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002557 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002558
2559 if not parsed_issue_arg.patchset:
2560 # Use current revision by default.
2561 revision_info = detail['revisions'][detail['current_revision']]
2562 patchset = int(revision_info['_number'])
2563 else:
2564 patchset = parsed_issue_arg.patchset
2565 for revision_info in detail['revisions'].itervalues():
2566 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2567 break
2568 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002569 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002570 (parsed_issue_arg.patchset, self.GetIssue()))
2571
2572 fetch_info = revision_info['fetch']['http']
2573 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2574 RunGit(['cherry-pick', 'FETCH_HEAD'])
2575 self.SetIssue(self.GetIssue())
2576 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002577 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002578 (self.GetIssue(), self.GetPatchset()))
2579 return 0
2580
2581 @staticmethod
2582 def ParseIssueURL(parsed_url):
2583 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2584 return None
2585 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2586 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2587 # Short urls like https://domain/<issue_number> can be used, but don't allow
2588 # specifying the patchset (you'd 404), but we allow that here.
2589 if parsed_url.path == '/':
2590 part = parsed_url.fragment
2591 else:
2592 part = parsed_url.path
2593 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2594 if match:
2595 return _ParsedIssueNumberArgument(
2596 issue=int(match.group(2)),
2597 patchset=int(match.group(4)) if match.group(4) else None,
2598 hostname=parsed_url.netloc)
2599 return None
2600
tandrii16e0b4e2016-06-07 10:34:28 -07002601 def _GerritCommitMsgHookCheck(self, offer_removal):
2602 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2603 if not os.path.exists(hook):
2604 return
2605 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2606 # custom developer made one.
2607 data = gclient_utils.FileRead(hook)
2608 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2609 return
2610 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002611 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002612 'and may interfere with it in subtle ways.\n'
2613 'We recommend you remove the commit-msg hook.')
2614 if offer_removal:
2615 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2616 if reply.lower().startswith('y'):
2617 gclient_utils.rm_file_or_tree(hook)
2618 print('Gerrit commit-msg hook removed.')
2619 else:
2620 print('OK, will keep Gerrit commit-msg hook in place.')
2621
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002622 def CMDUploadChange(self, options, args, change):
2623 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002624 if options.squash and options.no_squash:
2625 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002626
2627 if not options.squash and not options.no_squash:
2628 # Load default for user, repo, squash=true, in this order.
2629 options.squash = settings.GetSquashGerritUploads()
2630 elif options.no_squash:
2631 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002632
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002633 # We assume the remote called "origin" is the one we want.
2634 # It is probably not worthwhile to support different workflows.
2635 gerrit_remote = 'origin'
2636
2637 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002638 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002639
Aaron Gableb56ad332017-01-06 15:24:31 -08002640 # This may be None; default fallback value is determined in logic below.
2641 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002642 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002643
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002644 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002645 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002646 if self.GetIssue():
2647 # Try to get the message from a previous upload.
2648 message = self.GetDescription()
2649 if not message:
2650 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002651 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002652 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002653 if not title:
2654 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2655 title = ask_for_data(
2656 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002657 if title == default_title:
2658 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002659 change_id = self._GetChangeDetail()['change_id']
2660 while True:
2661 footer_change_ids = git_footers.get_footer_change_id(message)
2662 if footer_change_ids == [change_id]:
2663 break
2664 if not footer_change_ids:
2665 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002666 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002667 continue
2668 # There is already a valid footer but with different or several ids.
2669 # Doing this automatically is non-trivial as we don't want to lose
2670 # existing other footers, yet we want to append just 1 desired
2671 # Change-Id. Thus, just create a new footer, but let user verify the
2672 # new description.
2673 message = '%s\n\nChange-Id: %s' % (message, change_id)
2674 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002675 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002676 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002677 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002678 'Please, check the proposed correction to the description, '
2679 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2680 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2681 change_id))
2682 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2683 if not options.force:
2684 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002685 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002686 message = change_desc.description
2687 if not message:
2688 DieWithError("Description is empty. Aborting...")
2689 # Continue the while loop.
2690 # Sanity check of this code - we should end up with proper message
2691 # footer.
2692 assert [change_id] == git_footers.get_footer_change_id(message)
2693 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002694 else: # if not self.GetIssue()
2695 if options.message:
2696 message = options.message
2697 else:
2698 message = CreateDescriptionFromLog(args)
2699 if options.title:
2700 message = options.title + '\n\n' + message
2701 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002702 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002703 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002704 # On first upload, patchset title is always this string, while
2705 # --title flag gets converted to first line of message.
2706 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002707 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002708 if not change_desc.description:
2709 DieWithError("Description is empty. Aborting...")
2710 message = change_desc.description
2711 change_ids = git_footers.get_footer_change_id(message)
2712 if len(change_ids) > 1:
2713 DieWithError('too many Change-Id footers, at most 1 allowed.')
2714 if not change_ids:
2715 # Generate the Change-Id automatically.
2716 message = git_footers.add_footer_change_id(
2717 message, GenerateGerritChangeId(message))
2718 change_desc.set_description(message)
2719 change_ids = git_footers.get_footer_change_id(message)
2720 assert len(change_ids) == 1
2721 change_id = change_ids[0]
2722
2723 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2724 if remote is '.':
2725 # If our upstream branch is local, we base our squashed commit on its
2726 # squashed version.
2727 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2728 # Check the squashed hash of the parent.
2729 parent = RunGit(['config',
2730 'branch.%s.gerritsquashhash' % upstream_branch_name],
2731 error_ok=True).strip()
2732 # Verify that the upstream branch has been uploaded too, otherwise
2733 # Gerrit will create additional CLs when uploading.
2734 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2735 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002736 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002737 '\nUpload upstream branch %s first.\n'
2738 'It is likely that this branch has been rebased since its last '
2739 'upload, so you just need to upload it again.\n'
2740 '(If you uploaded it with --no-squash, then branch dependencies '
2741 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002742 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002743 else:
2744 parent = self.GetCommonAncestorWithUpstream()
2745
2746 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2747 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2748 '-m', message]).strip()
2749 else:
2750 change_desc = ChangeDescription(
2751 options.message or CreateDescriptionFromLog(args))
2752 if not change_desc.description:
2753 DieWithError("Description is empty. Aborting...")
2754
2755 if not git_footers.get_footer_change_id(change_desc.description):
2756 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002757 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2758 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002759 ref_to_push = 'HEAD'
2760 parent = '%s/%s' % (gerrit_remote, branch)
2761 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2762
2763 assert change_desc
2764 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2765 ref_to_push)]).splitlines()
2766 if len(commits) > 1:
2767 print('WARNING: This will upload %d commits. Run the following command '
2768 'to see which commits will be uploaded: ' % len(commits))
2769 print('git log %s..%s' % (parent, ref_to_push))
2770 print('You can also use `git squash-branch` to squash these into a '
2771 'single commit.')
2772 ask_for_data('About to upload; enter to confirm.')
2773
2774 if options.reviewers or options.tbr_owners:
2775 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2776 change)
2777
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002778 # Extra options that can be specified at push time. Doc:
2779 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2780 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002781 if change_desc.get_reviewers(tbr_only=True):
2782 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2783 refspec_opts.append('l=Code-Review+1')
2784
Aaron Gable9b713dd2016-12-14 16:04:21 -08002785 if title:
2786 if not re.match(r'^[\w ]+$', title):
2787 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002788 if not automatic_title:
2789 print('WARNING: Patchset title may only contain alphanumeric chars '
2790 'and spaces. Cleaned up title:\n%s' % title)
2791 if not options.force:
2792 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002793 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2794 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002795 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002796
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002797 if options.send_mail:
2798 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002799 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002800 refspec_opts.append('notify=ALL')
2801 else:
2802 refspec_opts.append('notify=NONE')
2803
tandrii99a72f22016-08-17 14:33:24 -07002804 reviewers = change_desc.get_reviewers()
2805 if reviewers:
2806 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002807
agablec6787972016-09-09 16:13:34 -07002808 if options.private:
2809 refspec_opts.append('draft')
2810
rmistry9eadede2016-09-19 11:22:43 -07002811 if options.topic:
2812 # Documentation on Gerrit topics is here:
2813 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2814 refspec_opts.append('topic=%s' % options.topic)
2815
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002816 refspec_suffix = ''
2817 if refspec_opts:
2818 refspec_suffix = '%' + ','.join(refspec_opts)
2819 assert ' ' not in refspec_suffix, (
2820 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002821 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002822
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002823 try:
2824 push_stdout = gclient_utils.CheckCallAndFilter(
2825 ['git', 'push', gerrit_remote, refspec],
2826 print_stdout=True,
2827 # Flush after every line: useful for seeing progress when running as
2828 # recipe.
2829 filter_fn=lambda _: sys.stdout.flush())
2830 except subprocess2.CalledProcessError:
2831 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002832 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002833
2834 if options.squash:
2835 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2836 change_numbers = [m.group(1)
2837 for m in map(regex.match, push_stdout.splitlines())
2838 if m]
2839 if len(change_numbers) != 1:
2840 DieWithError(
2841 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002842 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002843 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002844 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002845
2846 # Add cc's from the CC_LIST and --cc flag (if any).
2847 cc = self.GetCCList().split(',')
2848 if options.cc:
2849 cc.extend(options.cc)
2850 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002851 if change_desc.get_cced():
2852 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002853 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002854 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002855 self._GetGerritHost(), self.GetIssue(), cc,
2856 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002857 return 0
2858
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002859 def _AddChangeIdToCommitMessage(self, options, args):
2860 """Re-commits using the current message, assumes the commit hook is in
2861 place.
2862 """
2863 log_desc = options.message or CreateDescriptionFromLog(args)
2864 git_command = ['commit', '--amend', '-m', log_desc]
2865 RunGit(git_command)
2866 new_log_desc = CreateDescriptionFromLog(args)
2867 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002868 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002869 return new_log_desc
2870 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002871 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002872
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002873 def SetCQState(self, new_state):
2874 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002875 vote_map = {
2876 _CQState.NONE: 0,
2877 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002878 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002879 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002880 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2881 if new_state == _CQState.DRY_RUN:
2882 # Don't spam everybody reviewer/owner.
2883 kwargs['notify'] = 'NONE'
2884 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002885
tandriie113dfd2016-10-11 10:20:12 -07002886 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002887 try:
2888 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002889 except GerritChangeNotExists:
2890 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002891
2892 if data['status'] in ('ABANDONED', 'MERGED'):
2893 return 'CL %s is closed' % self.GetIssue()
2894
2895 def GetTryjobProperties(self, patchset=None):
2896 """Returns dictionary of properties to launch tryjob."""
2897 data = self._GetChangeDetail(['ALL_REVISIONS'])
2898 patchset = int(patchset or self.GetPatchset())
2899 assert patchset
2900 revision_data = None # Pylint wants it to be defined.
2901 for revision_data in data['revisions'].itervalues():
2902 if int(revision_data['_number']) == patchset:
2903 break
2904 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002905 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002906 (patchset, self.GetIssue()))
2907 return {
2908 'patch_issue': self.GetIssue(),
2909 'patch_set': patchset or self.GetPatchset(),
2910 'patch_project': data['project'],
2911 'patch_storage': 'gerrit',
2912 'patch_ref': revision_data['fetch']['http']['ref'],
2913 'patch_repository_url': revision_data['fetch']['http']['url'],
2914 'patch_gerrit_url': self.GetCodereviewServer(),
2915 }
tandriie113dfd2016-10-11 10:20:12 -07002916
tandriide281ae2016-10-12 06:02:30 -07002917 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002918 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002919
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002920
2921_CODEREVIEW_IMPLEMENTATIONS = {
2922 'rietveld': _RietveldChangelistImpl,
2923 'gerrit': _GerritChangelistImpl,
2924}
2925
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002926
iannuccie53c9352016-08-17 14:40:40 -07002927def _add_codereview_issue_select_options(parser, extra=""):
2928 _add_codereview_select_options(parser)
2929
2930 text = ('Operate on this issue number instead of the current branch\'s '
2931 'implicit issue.')
2932 if extra:
2933 text += ' '+extra
2934 parser.add_option('-i', '--issue', type=int, help=text)
2935
2936
2937def _process_codereview_issue_select_options(parser, options):
2938 _process_codereview_select_options(parser, options)
2939 if options.issue is not None and not options.forced_codereview:
2940 parser.error('--issue must be specified with either --rietveld or --gerrit')
2941
2942
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002943def _add_codereview_select_options(parser):
2944 """Appends --gerrit and --rietveld options to force specific codereview."""
2945 parser.codereview_group = optparse.OptionGroup(
2946 parser, 'EXPERIMENTAL! Codereview override options')
2947 parser.add_option_group(parser.codereview_group)
2948 parser.codereview_group.add_option(
2949 '--gerrit', action='store_true',
2950 help='Force the use of Gerrit for codereview')
2951 parser.codereview_group.add_option(
2952 '--rietveld', action='store_true',
2953 help='Force the use of Rietveld for codereview')
2954
2955
2956def _process_codereview_select_options(parser, options):
2957 if options.gerrit and options.rietveld:
2958 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2959 options.forced_codereview = None
2960 if options.gerrit:
2961 options.forced_codereview = 'gerrit'
2962 elif options.rietveld:
2963 options.forced_codereview = 'rietveld'
2964
2965
tandriif9aefb72016-07-01 09:06:51 -07002966def _get_bug_line_values(default_project, bugs):
2967 """Given default_project and comma separated list of bugs, yields bug line
2968 values.
2969
2970 Each bug can be either:
2971 * a number, which is combined with default_project
2972 * string, which is left as is.
2973
2974 This function may produce more than one line, because bugdroid expects one
2975 project per line.
2976
2977 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2978 ['v8:123', 'chromium:789']
2979 """
2980 default_bugs = []
2981 others = []
2982 for bug in bugs.split(','):
2983 bug = bug.strip()
2984 if bug:
2985 try:
2986 default_bugs.append(int(bug))
2987 except ValueError:
2988 others.append(bug)
2989
2990 if default_bugs:
2991 default_bugs = ','.join(map(str, default_bugs))
2992 if default_project:
2993 yield '%s:%s' % (default_project, default_bugs)
2994 else:
2995 yield default_bugs
2996 for other in sorted(others):
2997 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2998 yield other
2999
3000
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003001class ChangeDescription(object):
3002 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003003 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003004 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003005 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003006 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003007
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003008 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003009 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003010
agable@chromium.org42c20792013-09-12 17:34:49 +00003011 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003012 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003013 return '\n'.join(self._description_lines)
3014
3015 def set_description(self, desc):
3016 if isinstance(desc, basestring):
3017 lines = desc.splitlines()
3018 else:
3019 lines = [line.rstrip() for line in desc]
3020 while lines and not lines[0]:
3021 lines.pop(0)
3022 while lines and not lines[-1]:
3023 lines.pop(-1)
3024 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003025
piman@chromium.org336f9122014-09-04 02:16:55 +00003026 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003027 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003028 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003029 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003030 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003031 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003032
agable@chromium.org42c20792013-09-12 17:34:49 +00003033 # Get the set of R= and TBR= lines and remove them from the desciption.
3034 regexp = re.compile(self.R_LINE)
3035 matches = [regexp.match(line) for line in self._description_lines]
3036 new_desc = [l for i, l in enumerate(self._description_lines)
3037 if not matches[i]]
3038 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003039
agable@chromium.org42c20792013-09-12 17:34:49 +00003040 # Construct new unified R= and TBR= lines.
3041 r_names = []
3042 tbr_names = []
3043 for match in matches:
3044 if not match:
3045 continue
3046 people = cleanup_list([match.group(2).strip()])
3047 if match.group(1) == 'TBR':
3048 tbr_names.extend(people)
3049 else:
3050 r_names.extend(people)
3051 for name in r_names:
3052 if name not in reviewers:
3053 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003054 if add_owners_tbr:
3055 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003056 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003057 all_reviewers = set(tbr_names + reviewers)
3058 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3059 all_reviewers)
3060 tbr_names.extend(owners_db.reviewers_for(missing_files,
3061 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003062 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3063 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3064
3065 # Put the new lines in the description where the old first R= line was.
3066 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3067 if 0 <= line_loc < len(self._description_lines):
3068 if new_tbr_line:
3069 self._description_lines.insert(line_loc, new_tbr_line)
3070 if new_r_line:
3071 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003072 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003073 if new_r_line:
3074 self.append_footer(new_r_line)
3075 if new_tbr_line:
3076 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003077
tandriif9aefb72016-07-01 09:06:51 -07003078 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003079 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003080 self.set_description([
3081 '# Enter a description of the change.',
3082 '# This will be displayed on the codereview site.',
3083 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003084 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003085 '--------------------',
3086 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003087
agable@chromium.org42c20792013-09-12 17:34:49 +00003088 regexp = re.compile(self.BUG_LINE)
3089 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003090 prefix = settings.GetBugPrefix()
3091 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3092 for value in values:
3093 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3094 self.append_footer('BUG=%s' % value)
3095
agable@chromium.org42c20792013-09-12 17:34:49 +00003096 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003097 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003098 if not content:
3099 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003100 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003101
3102 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003103 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3104 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003105 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003106 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003107
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003108 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003109 """Adds a footer line to the description.
3110
3111 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3112 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3113 that Gerrit footers are always at the end.
3114 """
3115 parsed_footer_line = git_footers.parse_footer(line)
3116 if parsed_footer_line:
3117 # Line is a gerrit footer in the form: Footer-Key: any value.
3118 # Thus, must be appended observing Gerrit footer rules.
3119 self.set_description(
3120 git_footers.add_footer(self.description,
3121 key=parsed_footer_line[0],
3122 value=parsed_footer_line[1]))
3123 return
3124
3125 if not self._description_lines:
3126 self._description_lines.append(line)
3127 return
3128
3129 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3130 if gerrit_footers:
3131 # git_footers.split_footers ensures that there is an empty line before
3132 # actual (gerrit) footers, if any. We have to keep it that way.
3133 assert top_lines and top_lines[-1] == ''
3134 top_lines, separator = top_lines[:-1], top_lines[-1:]
3135 else:
3136 separator = [] # No need for separator if there are no gerrit_footers.
3137
3138 prev_line = top_lines[-1] if top_lines else ''
3139 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3140 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3141 top_lines.append('')
3142 top_lines.append(line)
3143 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003144
tandrii99a72f22016-08-17 14:33:24 -07003145 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003146 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003147 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003148 reviewers = [match.group(2).strip()
3149 for match in matches
3150 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003151 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003152
bradnelsond975b302016-10-23 12:20:23 -07003153 def get_cced(self):
3154 """Retrieves the list of reviewers."""
3155 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3156 cced = [match.group(2).strip() for match in matches if match]
3157 return cleanup_list(cced)
3158
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003159 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3160 """Updates this commit description given the parent.
3161
3162 This is essentially what Gnumbd used to do.
3163 Consult https://goo.gl/WMmpDe for more details.
3164 """
3165 assert parent_msg # No, orphan branch creation isn't supported.
3166 assert parent_hash
3167 assert dest_ref
3168 parent_footer_map = git_footers.parse_footers(parent_msg)
3169 # This will also happily parse svn-position, which GnumbD is no longer
3170 # supporting. While we'd generate correct footers, the verifier plugin
3171 # installed in Gerrit will block such commit (ie git push below will fail).
3172 parent_position = git_footers.get_position(parent_footer_map)
3173
3174 # Cherry-picks may have last line obscuring their prior footers,
3175 # from git_footers perspective. This is also what Gnumbd did.
3176 cp_line = None
3177 if (self._description_lines and
3178 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3179 cp_line = self._description_lines.pop()
3180
3181 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3182
3183 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3184 # user interference with actual footers we'd insert below.
3185 for i, (k, v) in enumerate(parsed_footers):
3186 if k.startswith('Cr-'):
3187 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3188
3189 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003190 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003191 if parent_position[0] == dest_ref:
3192 # Same branch as parent.
3193 number = int(parent_position[1]) + 1
3194 else:
3195 number = 1 # New branch, and extra lineage.
3196 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3197 int(parent_position[1])))
3198
3199 parsed_footers.append(('Cr-Commit-Position',
3200 '%s@{#%d}' % (dest_ref, number)))
3201 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3202
3203 self._description_lines = top_lines
3204 if cp_line:
3205 self._description_lines.append(cp_line)
3206 if self._description_lines[-1] != '':
3207 self._description_lines.append('') # Ensure footer separator.
3208 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3209
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003210
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003211def get_approving_reviewers(props):
3212 """Retrieves the reviewers that approved a CL from the issue properties with
3213 messages.
3214
3215 Note that the list may contain reviewers that are not committer, thus are not
3216 considered by the CQ.
3217 """
3218 return sorted(
3219 set(
3220 message['sender']
3221 for message in props['messages']
3222 if message['approval'] and message['sender'] in props['reviewers']
3223 )
3224 )
3225
3226
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003227def FindCodereviewSettingsFile(filename='codereview.settings'):
3228 """Finds the given file starting in the cwd and going up.
3229
3230 Only looks up to the top of the repository unless an
3231 'inherit-review-settings-ok' file exists in the root of the repository.
3232 """
3233 inherit_ok_file = 'inherit-review-settings-ok'
3234 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003235 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003236 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3237 root = '/'
3238 while True:
3239 if filename in os.listdir(cwd):
3240 if os.path.isfile(os.path.join(cwd, filename)):
3241 return open(os.path.join(cwd, filename))
3242 if cwd == root:
3243 break
3244 cwd = os.path.dirname(cwd)
3245
3246
3247def LoadCodereviewSettingsFromFile(fileobj):
3248 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003249 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003250
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003251 def SetProperty(name, setting, unset_error_ok=False):
3252 fullname = 'rietveld.' + name
3253 if setting in keyvals:
3254 RunGit(['config', fullname, keyvals[setting]])
3255 else:
3256 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3257
tandrii48df5812016-10-17 03:55:37 -07003258 if not keyvals.get('GERRIT_HOST', False):
3259 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003260 # Only server setting is required. Other settings can be absent.
3261 # In that case, we ignore errors raised during option deletion attempt.
3262 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003263 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003264 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3265 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003266 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003267 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3268 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003269 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003270 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3271 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003272
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003273 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003274 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003275
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003276 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003277 RunGit(['config', 'gerrit.squash-uploads',
3278 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003279
tandrii@chromium.org28253532016-04-14 13:46:56 +00003280 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003281 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003282 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3283
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003284 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003285 # should be of the form
3286 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3287 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003288 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3289 keyvals['ORIGIN_URL_CONFIG']])
3290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003291
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003292def urlretrieve(source, destination):
3293 """urllib is broken for SSL connections via a proxy therefore we
3294 can't use urllib.urlretrieve()."""
3295 with open(destination, 'w') as f:
3296 f.write(urllib2.urlopen(source).read())
3297
3298
ukai@chromium.org712d6102013-11-27 00:52:58 +00003299def hasSheBang(fname):
3300 """Checks fname is a #! script."""
3301 with open(fname) as f:
3302 return f.read(2).startswith('#!')
3303
3304
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003305# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3306def DownloadHooks(*args, **kwargs):
3307 pass
3308
3309
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003310def DownloadGerritHook(force):
3311 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003312
3313 Args:
3314 force: True to update hooks. False to install hooks if not present.
3315 """
3316 if not settings.GetIsGerrit():
3317 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003318 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003319 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3320 if not os.access(dst, os.X_OK):
3321 if os.path.exists(dst):
3322 if not force:
3323 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003324 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003325 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003326 if not hasSheBang(dst):
3327 DieWithError('Not a script: %s\n'
3328 'You need to download from\n%s\n'
3329 'into .git/hooks/commit-msg and '
3330 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003331 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3332 except Exception:
3333 if os.path.exists(dst):
3334 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003335 DieWithError('\nFailed to download hooks.\n'
3336 'You need to download from\n%s\n'
3337 'into .git/hooks/commit-msg and '
3338 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003339
3340
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003341def GetRietveldCodereviewSettingsInteractively():
3342 """Prompt the user for settings."""
3343 server = settings.GetDefaultServerUrl(error_ok=True)
3344 prompt = 'Rietveld server (host[:port])'
3345 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3346 newserver = ask_for_data(prompt + ':')
3347 if not server and not newserver:
3348 newserver = DEFAULT_SERVER
3349 if newserver:
3350 newserver = gclient_utils.UpgradeToHttps(newserver)
3351 if newserver != server:
3352 RunGit(['config', 'rietveld.server', newserver])
3353
3354 def SetProperty(initial, caption, name, is_url):
3355 prompt = caption
3356 if initial:
3357 prompt += ' ("x" to clear) [%s]' % initial
3358 new_val = ask_for_data(prompt + ':')
3359 if new_val == 'x':
3360 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3361 elif new_val:
3362 if is_url:
3363 new_val = gclient_utils.UpgradeToHttps(new_val)
3364 if new_val != initial:
3365 RunGit(['config', 'rietveld.' + name, new_val])
3366
3367 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3368 SetProperty(settings.GetDefaultPrivateFlag(),
3369 'Private flag (rietveld only)', 'private', False)
3370 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3371 'tree-status-url', False)
3372 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3373 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3374 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3375 'run-post-upload-hook', False)
3376
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003377
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003378@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003379def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003380 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381
tandrii5d0a0422016-09-14 06:24:35 -07003382 print('WARNING: git cl config works for Rietveld only')
3383 # TODO(tandrii): remove this once we switch to Gerrit.
3384 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003385 parser.add_option('--activate-update', action='store_true',
3386 help='activate auto-updating [rietveld] section in '
3387 '.git/config')
3388 parser.add_option('--deactivate-update', action='store_true',
3389 help='deactivate auto-updating [rietveld] section in '
3390 '.git/config')
3391 options, args = parser.parse_args(args)
3392
3393 if options.deactivate_update:
3394 RunGit(['config', 'rietveld.autoupdate', 'false'])
3395 return
3396
3397 if options.activate_update:
3398 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3399 return
3400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003401 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003402 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003403 return 0
3404
3405 url = args[0]
3406 if not url.endswith('codereview.settings'):
3407 url = os.path.join(url, 'codereview.settings')
3408
3409 # Load code review settings and download hooks (if available).
3410 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3411 return 0
3412
3413
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003414def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003415 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003416 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3417 branch = ShortBranchName(branchref)
3418 _, args = parser.parse_args(args)
3419 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003420 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003421 return RunGit(['config', 'branch.%s.base-url' % branch],
3422 error_ok=False).strip()
3423 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003424 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003425 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3426 error_ok=False).strip()
3427
3428
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003429def color_for_status(status):
3430 """Maps a Changelist status to color, for CMDstatus and other tools."""
3431 return {
3432 'unsent': Fore.RED,
3433 'waiting': Fore.BLUE,
3434 'reply': Fore.YELLOW,
3435 'lgtm': Fore.GREEN,
3436 'commit': Fore.MAGENTA,
3437 'closed': Fore.CYAN,
3438 'error': Fore.WHITE,
3439 }.get(status, Fore.WHITE)
3440
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003441
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003442def get_cl_statuses(changes, fine_grained, max_processes=None):
3443 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003444
3445 If fine_grained is true, this will fetch CL statuses from the server.
3446 Otherwise, simply indicate if there's a matching url for the given branches.
3447
3448 If max_processes is specified, it is used as the maximum number of processes
3449 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3450 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003451
3452 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003453 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003454 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003455 upload.verbosity = 0
3456
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003457 if not changes:
3458 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003459
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003460 if not fine_grained:
3461 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003462 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003463 for cl in changes:
3464 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003465 return
3466
3467 # First, sort out authentication issues.
3468 logging.debug('ensuring credentials exist')
3469 for cl in changes:
3470 cl.EnsureAuthenticated(force=False, refresh=True)
3471
3472 def fetch(cl):
3473 try:
3474 return (cl, cl.GetStatus())
3475 except:
3476 # See http://crbug.com/629863.
3477 logging.exception('failed to fetch status for %s:', cl)
3478 raise
3479
3480 threads_count = len(changes)
3481 if max_processes:
3482 threads_count = max(1, min(threads_count, max_processes))
3483 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3484
3485 pool = ThreadPool(threads_count)
3486 fetched_cls = set()
3487 try:
3488 it = pool.imap_unordered(fetch, changes).__iter__()
3489 while True:
3490 try:
3491 cl, status = it.next(timeout=5)
3492 except multiprocessing.TimeoutError:
3493 break
3494 fetched_cls.add(cl)
3495 yield cl, status
3496 finally:
3497 pool.close()
3498
3499 # Add any branches that failed to fetch.
3500 for cl in set(changes) - fetched_cls:
3501 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003502
rmistry@google.com2dd99862015-06-22 12:22:18 +00003503
3504def upload_branch_deps(cl, args):
3505 """Uploads CLs of local branches that are dependents of the current branch.
3506
3507 If the local branch dependency tree looks like:
3508 test1 -> test2.1 -> test3.1
3509 -> test3.2
3510 -> test2.2 -> test3.3
3511
3512 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3513 run on the dependent branches in this order:
3514 test2.1, test3.1, test3.2, test2.2, test3.3
3515
3516 Note: This function does not rebase your local dependent branches. Use it when
3517 you make a change to the parent branch that will not conflict with its
3518 dependent branches, and you would like their dependencies updated in
3519 Rietveld.
3520 """
3521 if git_common.is_dirty_git_tree('upload-branch-deps'):
3522 return 1
3523
3524 root_branch = cl.GetBranch()
3525 if root_branch is None:
3526 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3527 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003528 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003529 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3530 'patchset dependencies without an uploaded CL.')
3531
3532 branches = RunGit(['for-each-ref',
3533 '--format=%(refname:short) %(upstream:short)',
3534 'refs/heads'])
3535 if not branches:
3536 print('No local branches found.')
3537 return 0
3538
3539 # Create a dictionary of all local branches to the branches that are dependent
3540 # on it.
3541 tracked_to_dependents = collections.defaultdict(list)
3542 for b in branches.splitlines():
3543 tokens = b.split()
3544 if len(tokens) == 2:
3545 branch_name, tracked = tokens
3546 tracked_to_dependents[tracked].append(branch_name)
3547
vapiera7fbd5a2016-06-16 09:17:49 -07003548 print()
3549 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003550 dependents = []
3551 def traverse_dependents_preorder(branch, padding=''):
3552 dependents_to_process = tracked_to_dependents.get(branch, [])
3553 padding += ' '
3554 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003555 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003556 dependents.append(dependent)
3557 traverse_dependents_preorder(dependent, padding)
3558 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003559 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003560
3561 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003562 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003563 return 0
3564
vapiera7fbd5a2016-06-16 09:17:49 -07003565 print('This command will checkout all dependent branches and run '
3566 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003567 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3568
andybons@chromium.org962f9462016-02-03 20:00:42 +00003569 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003570 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003571 args.extend(['-t', 'Updated patchset dependency'])
3572
rmistry@google.com2dd99862015-06-22 12:22:18 +00003573 # Record all dependents that failed to upload.
3574 failures = {}
3575 # Go through all dependents, checkout the branch and upload.
3576 try:
3577 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003578 print()
3579 print('--------------------------------------')
3580 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003581 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003582 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003583 try:
3584 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003585 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003586 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003587 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003588 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003589 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003590 finally:
3591 # Swap back to the original root branch.
3592 RunGit(['checkout', '-q', root_branch])
3593
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print()
3595 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003596 for dependent_branch in dependents:
3597 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003598 print(' %s : %s' % (dependent_branch, upload_status))
3599 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003600
3601 return 0
3602
3603
kmarshall3bff56b2016-06-06 18:31:47 -07003604def CMDarchive(parser, args):
3605 """Archives and deletes branches associated with closed changelists."""
3606 parser.add_option(
3607 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003608 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003609 parser.add_option(
3610 '-f', '--force', action='store_true',
3611 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003612 parser.add_option(
3613 '-d', '--dry-run', action='store_true',
3614 help='Skip the branch tagging and removal steps.')
3615 parser.add_option(
3616 '-t', '--notags', action='store_true',
3617 help='Do not tag archived branches. '
3618 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003619
3620 auth.add_auth_options(parser)
3621 options, args = parser.parse_args(args)
3622 if args:
3623 parser.error('Unsupported args: %s' % ' '.join(args))
3624 auth_config = auth.extract_auth_config_from_options(options)
3625
3626 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3627 if not branches:
3628 return 0
3629
vapiera7fbd5a2016-06-16 09:17:49 -07003630 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003631 changes = [Changelist(branchref=b, auth_config=auth_config)
3632 for b in branches.splitlines()]
3633 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3634 statuses = get_cl_statuses(changes,
3635 fine_grained=True,
3636 max_processes=options.maxjobs)
3637 proposal = [(cl.GetBranch(),
3638 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3639 for cl, status in statuses
3640 if status == 'closed']
3641 proposal.sort()
3642
3643 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003644 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003645 return 0
3646
3647 current_branch = GetCurrentBranch()
3648
vapiera7fbd5a2016-06-16 09:17:49 -07003649 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003650 if options.notags:
3651 for next_item in proposal:
3652 print(' ' + next_item[0])
3653 else:
3654 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3655 for next_item in proposal:
3656 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003657
kmarshall9249e012016-08-23 12:02:16 -07003658 # Quit now on precondition failure or if instructed by the user, either
3659 # via an interactive prompt or by command line flags.
3660 if options.dry_run:
3661 print('\nNo changes were made (dry run).\n')
3662 return 0
3663 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003664 print('You are currently on a branch \'%s\' which is associated with a '
3665 'closed codereview issue, so archive cannot proceed. Please '
3666 'checkout another branch and run this command again.' %
3667 current_branch)
3668 return 1
kmarshall9249e012016-08-23 12:02:16 -07003669 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003670 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3671 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003672 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003673 return 1
3674
3675 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003676 if not options.notags:
3677 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003678 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003679
vapiera7fbd5a2016-06-16 09:17:49 -07003680 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003681
3682 return 0
3683
3684
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003685def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003686 """Show status of changelists.
3687
3688 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003689 - Red not sent for review or broken
3690 - Blue waiting for review
3691 - Yellow waiting for you to reply to review
3692 - Green LGTM'ed
3693 - Magenta in the commit queue
3694 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003695
3696 Also see 'git cl comments'.
3697 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003698 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003699 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003700 parser.add_option('-f', '--fast', action='store_true',
3701 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003702 parser.add_option(
3703 '-j', '--maxjobs', action='store', type=int,
3704 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003705
3706 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003707 _add_codereview_issue_select_options(
3708 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003709 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003710 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003711 if args:
3712 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003713 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003714
iannuccie53c9352016-08-17 14:40:40 -07003715 if options.issue is not None and not options.field:
3716 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003717
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003718 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003719 cl = Changelist(auth_config=auth_config, issue=options.issue,
3720 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003721 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003722 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003723 elif options.field == 'id':
3724 issueid = cl.GetIssue()
3725 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003726 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003727 elif options.field == 'patch':
3728 patchset = cl.GetPatchset()
3729 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003730 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003731 elif options.field == 'status':
3732 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003733 elif options.field == 'url':
3734 url = cl.GetIssueURL()
3735 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003736 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003737 return 0
3738
3739 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3740 if not branches:
3741 print('No local branch found.')
3742 return 0
3743
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003744 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003745 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003746 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003747 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003748 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003749 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003750 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003751
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003752 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003753 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3754 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3755 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003756 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003757 c, status = output.next()
3758 branch_statuses[c.GetBranch()] = status
3759 status = branch_statuses.pop(branch)
3760 url = cl.GetIssueURL()
3761 if url and (not status or status == 'error'):
3762 # The issue probably doesn't exist anymore.
3763 url += ' (broken)'
3764
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003765 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003766 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003767 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003768 color = ''
3769 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003770 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003771 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003772 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003773 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003774
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003775
3776 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003777 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003778 print('Current branch: %s' % branch)
3779 for cl in changes:
3780 if cl.GetBranch() == branch:
3781 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003782 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003783 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003784 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003785 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003786 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003787 print('Issue description:')
3788 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003789 return 0
3790
3791
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003792def colorize_CMDstatus_doc():
3793 """To be called once in main() to add colors to git cl status help."""
3794 colors = [i for i in dir(Fore) if i[0].isupper()]
3795
3796 def colorize_line(line):
3797 for color in colors:
3798 if color in line.upper():
3799 # Extract whitespaces first and the leading '-'.
3800 indent = len(line) - len(line.lstrip(' ')) + 1
3801 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3802 return line
3803
3804 lines = CMDstatus.__doc__.splitlines()
3805 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3806
3807
phajdan.jre328cf92016-08-22 04:12:17 -07003808def write_json(path, contents):
3809 with open(path, 'w') as f:
3810 json.dump(contents, f)
3811
3812
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003813@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003814def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003815 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003816
3817 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003818 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003819 parser.add_option('-r', '--reverse', action='store_true',
3820 help='Lookup the branch(es) for the specified issues. If '
3821 'no issues are specified, all branches with mapped '
3822 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003823 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003824 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003825 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003826 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003827
dnj@chromium.org406c4402015-03-03 17:22:28 +00003828 if options.reverse:
3829 branches = RunGit(['for-each-ref', 'refs/heads',
3830 '--format=%(refname:short)']).splitlines()
3831
3832 # Reverse issue lookup.
3833 issue_branch_map = {}
3834 for branch in branches:
3835 cl = Changelist(branchref=branch)
3836 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3837 if not args:
3838 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003839 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003840 for issue in args:
3841 if not issue:
3842 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003843 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003844 print('Branch for issue number %s: %s' % (
3845 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003846 if options.json:
3847 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003848 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003849 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003850 if len(args) > 0:
3851 try:
3852 issue = int(args[0])
3853 except ValueError:
3854 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003855 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003856 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003857 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003858 if options.json:
3859 write_json(options.json, {
3860 'issue': cl.GetIssue(),
3861 'issue_url': cl.GetIssueURL(),
3862 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003863 return 0
3864
3865
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003866def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003867 """Shows or posts review comments for any changelist."""
3868 parser.add_option('-a', '--add-comment', dest='comment',
3869 help='comment to add to an issue')
3870 parser.add_option('-i', dest='issue',
3871 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003872 parser.add_option('-j', '--json-file',
3873 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003874 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003875 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003876 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003877
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003878 issue = None
3879 if options.issue:
3880 try:
3881 issue = int(options.issue)
3882 except ValueError:
3883 DieWithError('A review issue id is expected to be a number')
3884
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003885 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003886
3887 if options.comment:
3888 cl.AddComment(options.comment)
3889 return 0
3890
3891 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003892 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003893 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003894 summary.append({
3895 'date': message['date'],
3896 'lgtm': False,
3897 'message': message['text'],
3898 'not_lgtm': False,
3899 'sender': message['sender'],
3900 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003901 if message['disapproval']:
3902 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003903 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003904 elif message['approval']:
3905 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003906 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003907 elif message['sender'] == data['owner_email']:
3908 color = Fore.MAGENTA
3909 else:
3910 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003911 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003912 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003913 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003914 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003915 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003916 if options.json_file:
3917 with open(options.json_file, 'wb') as f:
3918 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003919 return 0
3920
3921
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003922@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003923def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003924 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003925 parser.add_option('-d', '--display', action='store_true',
3926 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003927 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003928 help='New description to set for this issue (- for stdin, '
3929 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003930 parser.add_option('-f', '--force', action='store_true',
3931 help='Delete any unpublished Gerrit edits for this issue '
3932 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003933
3934 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003935 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003936 options, args = parser.parse_args(args)
3937 _process_codereview_select_options(parser, options)
3938
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003939 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003940 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003941 target_issue_arg = ParseIssueNumberArgument(args[0])
3942 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003943 parser.print_help()
3944 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003945
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003946 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003947
martiniss6eda05f2016-06-30 10:18:35 -07003948 kwargs = {
3949 'auth_config': auth_config,
3950 'codereview': options.forced_codereview,
3951 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003952 if target_issue_arg:
3953 kwargs['issue'] = target_issue_arg.issue
3954 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07003955
3956 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003957
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003958 if not cl.GetIssue():
3959 DieWithError('This branch has no associated changelist.')
3960 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003961
smut@google.com34fb6b12015-07-13 20:03:26 +00003962 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003963 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003964 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003965
3966 if options.new_description:
3967 text = options.new_description
3968 if text == '-':
3969 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003970 elif text == '+':
3971 base_branch = cl.GetCommonAncestorWithUpstream()
3972 change = cl.GetChange(base_branch, None, local_description=True)
3973 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003974
3975 description.set_description(text)
3976 else:
3977 description.prompt()
3978
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003979 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003980 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003981 return 0
3982
3983
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984def CreateDescriptionFromLog(args):
3985 """Pulls out the commit log to use as a base for the CL description."""
3986 log_args = []
3987 if len(args) == 1 and not args[0].endswith('.'):
3988 log_args = [args[0] + '..']
3989 elif len(args) == 1 and args[0].endswith('...'):
3990 log_args = [args[0][:-1]]
3991 elif len(args) == 2:
3992 log_args = [args[0] + '..' + args[1]]
3993 else:
3994 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003995 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003996
3997
thestig@chromium.org44202a22014-03-11 19:22:18 +00003998def CMDlint(parser, args):
3999 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004000 parser.add_option('--filter', action='append', metavar='-x,+y',
4001 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004002 auth.add_auth_options(parser)
4003 options, args = parser.parse_args(args)
4004 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004005
4006 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004007 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004008 try:
4009 import cpplint
4010 import cpplint_chromium
4011 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004012 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004013 return 1
4014
4015 # Change the current working directory before calling lint so that it
4016 # shows the correct base.
4017 previous_cwd = os.getcwd()
4018 os.chdir(settings.GetRoot())
4019 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004020 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004021 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4022 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004023 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004024 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004025 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004026
4027 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004028 command = args + files
4029 if options.filter:
4030 command = ['--filter=' + ','.join(options.filter)] + command
4031 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004032
4033 white_regex = re.compile(settings.GetLintRegex())
4034 black_regex = re.compile(settings.GetLintIgnoreRegex())
4035 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4036 for filename in filenames:
4037 if white_regex.match(filename):
4038 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004039 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004040 else:
4041 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4042 extra_check_functions)
4043 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004044 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004045 finally:
4046 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004047 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004048 if cpplint._cpplint_state.error_count != 0:
4049 return 1
4050 return 0
4051
4052
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004053def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004054 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004055 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004056 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004057 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004058 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004059 auth.add_auth_options(parser)
4060 options, args = parser.parse_args(args)
4061 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004062
sbc@chromium.org71437c02015-04-09 19:29:40 +00004063 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004064 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004065 return 1
4066
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004067 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004068 if args:
4069 base_branch = args[0]
4070 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004071 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004072 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004073
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004074 cl.RunHook(
4075 committing=not options.upload,
4076 may_prompt=False,
4077 verbose=options.verbose,
4078 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004079 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004080
4081
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004082def GenerateGerritChangeId(message):
4083 """Returns Ixxxxxx...xxx change id.
4084
4085 Works the same way as
4086 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4087 but can be called on demand on all platforms.
4088
4089 The basic idea is to generate git hash of a state of the tree, original commit
4090 message, author/committer info and timestamps.
4091 """
4092 lines = []
4093 tree_hash = RunGitSilent(['write-tree'])
4094 lines.append('tree %s' % tree_hash.strip())
4095 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4096 if code == 0:
4097 lines.append('parent %s' % parent.strip())
4098 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4099 lines.append('author %s' % author.strip())
4100 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4101 lines.append('committer %s' % committer.strip())
4102 lines.append('')
4103 # Note: Gerrit's commit-hook actually cleans message of some lines and
4104 # whitespace. This code is not doing this, but it clearly won't decrease
4105 # entropy.
4106 lines.append(message)
4107 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4108 stdin='\n'.join(lines))
4109 return 'I%s' % change_hash.strip()
4110
4111
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004112def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004113 """Computes the remote branch ref to use for the CL.
4114
4115 Args:
4116 remote (str): The git remote for the CL.
4117 remote_branch (str): The git remote branch for the CL.
4118 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004119 """
4120 if not (remote and remote_branch):
4121 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004122
wittman@chromium.org455dc922015-01-26 20:15:50 +00004123 if target_branch:
4124 # Cannonicalize branch references to the equivalent local full symbolic
4125 # refs, which are then translated into the remote full symbolic refs
4126 # below.
4127 if '/' not in target_branch:
4128 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4129 else:
4130 prefix_replacements = (
4131 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4132 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4133 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4134 )
4135 match = None
4136 for regex, replacement in prefix_replacements:
4137 match = re.search(regex, target_branch)
4138 if match:
4139 remote_branch = target_branch.replace(match.group(0), replacement)
4140 break
4141 if not match:
4142 # This is a branch path but not one we recognize; use as-is.
4143 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004144 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4145 # Handle the refs that need to land in different refs.
4146 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004147
wittman@chromium.org455dc922015-01-26 20:15:50 +00004148 # Create the true path to the remote branch.
4149 # Does the following translation:
4150 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4151 # * refs/remotes/origin/master -> refs/heads/master
4152 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4153 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4154 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4155 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4156 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4157 'refs/heads/')
4158 elif remote_branch.startswith('refs/remotes/branch-heads'):
4159 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004160
wittman@chromium.org455dc922015-01-26 20:15:50 +00004161 return remote_branch
4162
4163
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004164def cleanup_list(l):
4165 """Fixes a list so that comma separated items are put as individual items.
4166
4167 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4168 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4169 """
4170 items = sum((i.split(',') for i in l), [])
4171 stripped_items = (i.strip() for i in items)
4172 return sorted(filter(None, stripped_items))
4173
4174
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004175@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004176def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004177 """Uploads the current changelist to codereview.
4178
4179 Can skip dependency patchset uploads for a branch by running:
4180 git config branch.branch_name.skip-deps-uploads True
4181 To unset run:
4182 git config --unset branch.branch_name.skip-deps-uploads
4183 Can also set the above globally by using the --global flag.
4184 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004185 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4186 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004187 parser.add_option('--bypass-watchlists', action='store_true',
4188 dest='bypass_watchlists',
4189 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004190 parser.add_option('-f', action='store_true', dest='force',
4191 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004192 parser.add_option('--message', '-m', dest='message',
4193 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004194 parser.add_option('-b', '--bug',
4195 help='pre-populate the bug number(s) for this issue. '
4196 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004197 parser.add_option('--message-file', dest='message_file',
4198 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004199 parser.add_option('--title', '-t', dest='title',
4200 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004201 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004202 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004203 help='reviewer email addresses')
4204 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004205 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004206 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004207 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004208 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004209 parser.add_option('--emulate_svn_auto_props',
4210 '--emulate-svn-auto-props',
4211 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004212 dest="emulate_svn_auto_props",
4213 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004214 parser.add_option('-c', '--use-commit-queue', action='store_true',
4215 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004216 parser.add_option('--private', action='store_true',
4217 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004218 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004219 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004220 metavar='TARGET',
4221 help='Apply CL to remote ref TARGET. ' +
4222 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004223 parser.add_option('--squash', action='store_true',
4224 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004225 parser.add_option('--no-squash', action='store_true',
4226 help='Don\'t squash multiple commits into one ' +
4227 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004228 parser.add_option('--topic', default=None,
4229 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004230 parser.add_option('--email', default=None,
4231 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004232 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4233 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004234 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4235 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004236 help='Send the patchset to do a CQ dry run right after '
4237 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004238 parser.add_option('--dependencies', action='store_true',
4239 help='Uploads CLs of all the local branches that depend on '
4240 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004241
rmistry@google.com2dd99862015-06-22 12:22:18 +00004242 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004243 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004244 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004245 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004246 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004247 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004248 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004249
sbc@chromium.org71437c02015-04-09 19:29:40 +00004250 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004251 return 1
4252
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004253 options.reviewers = cleanup_list(options.reviewers)
4254 options.cc = cleanup_list(options.cc)
4255
tandriib80458a2016-06-23 12:20:07 -07004256 if options.message_file:
4257 if options.message:
4258 parser.error('only one of --message and --message-file allowed.')
4259 options.message = gclient_utils.FileRead(options.message_file)
4260 options.message_file = None
4261
tandrii4d0545a2016-07-06 03:56:49 -07004262 if options.cq_dry_run and options.use_commit_queue:
4263 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4264
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004265 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4266 settings.GetIsGerrit()
4267
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004268 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004269 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004270
4271
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004272@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004273def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004274 """DEPRECATED: Used to commit the current changelist via git-svn."""
4275 message = ('git-cl no longer supports committing to SVN repositories via '
4276 'git-svn. You probably want to use `git cl land` instead.')
4277 print(message)
4278 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004279
4280
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004281@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004282def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004283 """Commits the current changelist via git.
4284
4285 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4286 upstream and closes the issue automatically and atomically.
4287
4288 Otherwise (in case of Rietveld):
4289 Squashes branch into a single commit.
4290 Updates commit message with metadata (e.g. pointer to review).
4291 Pushes the code upstream.
4292 Updates review and closes.
4293 """
4294 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4295 help='bypass upload presubmit hook')
4296 parser.add_option('-m', dest='message',
4297 help="override review description")
4298 parser.add_option('-f', action='store_true', dest='force',
4299 help="force yes to questions (don't prompt)")
4300 parser.add_option('-c', dest='contributor',
4301 help="external contributor for patch (appended to " +
4302 "description and used as author for git). Should be " +
4303 "formatted as 'First Last <email@example.com>'")
4304 add_git_similarity(parser)
4305 auth.add_auth_options(parser)
4306 (options, args) = parser.parse_args(args)
4307 auth_config = auth.extract_auth_config_from_options(options)
4308
4309 cl = Changelist(auth_config=auth_config)
4310
4311 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4312 if cl.IsGerrit():
4313 if options.message:
4314 # This could be implemented, but it requires sending a new patch to
4315 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4316 # Besides, Gerrit has the ability to change the commit message on submit
4317 # automatically, thus there is no need to support this option (so far?).
4318 parser.error('-m MESSAGE option is not supported for Gerrit.')
4319 if options.contributor:
4320 parser.error(
4321 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4322 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4323 'the contributor\'s "name <email>". If you can\'t upload such a '
4324 'commit for review, contact your repository admin and request'
4325 '"Forge-Author" permission.')
4326 if not cl.GetIssue():
4327 DieWithError('You must upload the change first to Gerrit.\n'
4328 ' If you would rather have `git cl land` upload '
4329 'automatically for you, see http://crbug.com/642759')
4330 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4331 options.verbose)
4332
4333 current = cl.GetBranch()
4334 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4335 if remote == '.':
4336 print()
4337 print('Attempting to push branch %r into another local branch!' % current)
4338 print()
4339 print('Either reparent this branch on top of origin/master:')
4340 print(' git reparent-branch --root')
4341 print()
4342 print('OR run `git rebase-update` if you think the parent branch is ')
4343 print('already committed.')
4344 print()
4345 print(' Current parent: %r' % upstream_branch)
4346 return 1
4347
4348 if not args:
4349 # Default to merging against our best guess of the upstream branch.
4350 args = [cl.GetUpstreamBranch()]
4351
4352 if options.contributor:
4353 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4354 print("Please provide contibutor as 'First Last <email@example.com>'")
4355 return 1
4356
4357 base_branch = args[0]
4358
4359 if git_common.is_dirty_git_tree('land'):
4360 return 1
4361
4362 # This rev-list syntax means "show all commits not in my branch that
4363 # are in base_branch".
4364 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4365 base_branch]).splitlines()
4366 if upstream_commits:
4367 print('Base branch "%s" has %d commits '
4368 'not in this branch.' % (base_branch, len(upstream_commits)))
4369 print('Run "git merge %s" before attempting to land.' % base_branch)
4370 return 1
4371
4372 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4373 if not options.bypass_hooks:
4374 author = None
4375 if options.contributor:
4376 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4377 hook_results = cl.RunHook(
4378 committing=True,
4379 may_prompt=not options.force,
4380 verbose=options.verbose,
4381 change=cl.GetChange(merge_base, author))
4382 if not hook_results.should_continue():
4383 return 1
4384
4385 # Check the tree status if the tree status URL is set.
4386 status = GetTreeStatus()
4387 if 'closed' == status:
4388 print('The tree is closed. Please wait for it to reopen. Use '
4389 '"git cl land --bypass-hooks" to commit on a closed tree.')
4390 return 1
4391 elif 'unknown' == status:
4392 print('Unable to determine tree status. Please verify manually and '
4393 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4394 return 1
4395
4396 change_desc = ChangeDescription(options.message)
4397 if not change_desc.description and cl.GetIssue():
4398 change_desc = ChangeDescription(cl.GetDescription())
4399
4400 if not change_desc.description:
4401 if not cl.GetIssue() and options.bypass_hooks:
4402 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4403 else:
4404 print('No description set.')
4405 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4406 return 1
4407
4408 # Keep a separate copy for the commit message, because the commit message
4409 # contains the link to the Rietveld issue, while the Rietveld message contains
4410 # the commit viewvc url.
4411 if cl.GetIssue():
4412 change_desc.update_reviewers(cl.GetApprovingReviewers())
4413
4414 commit_desc = ChangeDescription(change_desc.description)
4415 if cl.GetIssue():
4416 # Xcode won't linkify this URL unless there is a non-whitespace character
4417 # after it. Add a period on a new line to circumvent this. Also add a space
4418 # before the period to make sure that Gitiles continues to correctly resolve
4419 # the URL.
4420 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4421 if options.contributor:
4422 commit_desc.append_footer('Patch from %s.' % options.contributor)
4423
4424 print('Description:')
4425 print(commit_desc.description)
4426
4427 branches = [merge_base, cl.GetBranchRef()]
4428 if not options.force:
4429 print_stats(options.similarity, options.find_copies, branches)
4430
4431 # We want to squash all this branch's commits into one commit with the proper
4432 # description. We do this by doing a "reset --soft" to the base branch (which
4433 # keeps the working copy the same), then landing that.
4434 MERGE_BRANCH = 'git-cl-commit'
4435 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4436 # Delete the branches if they exist.
4437 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4438 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4439 result = RunGitWithCode(showref_cmd)
4440 if result[0] == 0:
4441 RunGit(['branch', '-D', branch])
4442
4443 # We might be in a directory that's present in this branch but not in the
4444 # trunk. Move up to the top of the tree so that git commands that expect a
4445 # valid CWD won't fail after we check out the merge branch.
4446 rel_base_path = settings.GetRelativeRoot()
4447 if rel_base_path:
4448 os.chdir(rel_base_path)
4449
4450 # Stuff our change into the merge branch.
4451 # We wrap in a try...finally block so if anything goes wrong,
4452 # we clean up the branches.
4453 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004454 revision = None
4455 try:
4456 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4457 RunGit(['reset', '--soft', merge_base])
4458 if options.contributor:
4459 RunGit(
4460 [
4461 'commit', '--author', options.contributor,
4462 '-m', commit_desc.description,
4463 ])
4464 else:
4465 RunGit(['commit', '-m', commit_desc.description])
4466
4467 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4468 mirror = settings.GetGitMirror(remote)
4469 if mirror:
4470 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004471 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004472 else:
4473 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004474 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004475 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4476
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004477 if git_numberer_enabled:
4478 # TODO(tandrii): maybe do autorebase + retry on failure
4479 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004480 logging.debug('Adding git number footers')
4481 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4482 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4483 branch)
4484 # Ensure timestamps are monotonically increasing.
4485 timestamp = max(1 + _get_committer_timestamp(merge_base),
4486 _get_committer_timestamp('HEAD'))
4487 _git_amend_head(commit_desc.description, timestamp)
4488 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004489
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004490 retcode, output = RunGitWithCode(
4491 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004492 if retcode == 0:
4493 revision = RunGit(['rev-parse', 'HEAD']).strip()
4494 logging.debug(output)
4495 except: # pylint: disable=bare-except
4496 if _IS_BEING_TESTED:
4497 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4498 + '-' * 30 + '8<' + '-' * 30)
4499 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4500 raise
4501 finally:
4502 # And then swap back to the original branch and clean up.
4503 RunGit(['checkout', '-q', cl.GetBranch()])
4504 RunGit(['branch', '-D', MERGE_BRANCH])
4505
4506 if not revision:
4507 print('Failed to push. If this persists, please file a bug.')
4508 return 1
4509
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004510 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004511 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004512 if viewvc_url and revision:
4513 change_desc.append_footer(
4514 'Committed: %s%s' % (viewvc_url, revision))
4515 elif revision:
4516 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004517 print('Closing issue '
4518 '(you may be prompted for your codereview password)...')
4519 cl.UpdateDescription(change_desc.description)
4520 cl.CloseIssue()
4521 props = cl.GetIssueProperties()
4522 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004523 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4524 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004525 if options.bypass_hooks:
4526 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4527 else:
4528 comment += ' (presubmit successful).'
4529 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4530
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004531 if os.path.isfile(POSTUPSTREAM_HOOK):
4532 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4533
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004534 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004535
4536
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004537@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004538def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004539 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004540 parser.add_option('-b', dest='newbranch',
4541 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004542 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004543 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004544 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4545 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004546 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004547 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004548 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004549 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004550 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004551 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004552
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004553
4554 group = optparse.OptionGroup(
4555 parser,
4556 'Options for continuing work on the current issue uploaded from a '
4557 'different clone (e.g. different machine). Must be used independently '
4558 'from the other options. No issue number should be specified, and the '
4559 'branch must have an issue number associated with it')
4560 group.add_option('--reapply', action='store_true', dest='reapply',
4561 help='Reset the branch and reapply the issue.\n'
4562 'CAUTION: This will undo any local changes in this '
4563 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004564
4565 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004566 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004567 parser.add_option_group(group)
4568
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004569 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004570 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004571 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004572 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004573 auth_config = auth.extract_auth_config_from_options(options)
4574
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004575
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004576 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004577 if options.newbranch:
4578 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004579 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004580 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004581
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004582 cl = Changelist(auth_config=auth_config,
4583 codereview=options.forced_codereview)
4584 if not cl.GetIssue():
4585 parser.error('current branch must have an associated issue')
4586
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004587 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004588 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004589 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004590
4591 RunGit(['reset', '--hard', upstream])
4592 if options.pull:
4593 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004594
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004595 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4596 options.directory)
4597
4598 if len(args) != 1 or not args[0]:
4599 parser.error('Must specify issue number or url')
4600
4601 # We don't want uncommitted changes mixed up with the patch.
4602 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004603 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004604
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004605 if options.newbranch:
4606 if options.force:
4607 RunGit(['branch', '-D', options.newbranch],
4608 stderr=subprocess2.PIPE, error_ok=True)
4609 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004610 elif not GetCurrentBranch():
4611 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004612
4613 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4614
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004615 if cl.IsGerrit():
4616 if options.reject:
4617 parser.error('--reject is not supported with Gerrit codereview.')
4618 if options.nocommit:
4619 parser.error('--nocommit is not supported with Gerrit codereview.')
4620 if options.directory:
4621 parser.error('--directory is not supported with Gerrit codereview.')
4622
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004623 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004624 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004625
4626
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004627def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004628 """Fetches the tree status and returns either 'open', 'closed',
4629 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004630 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004631 if url:
4632 status = urllib2.urlopen(url).read().lower()
4633 if status.find('closed') != -1 or status == '0':
4634 return 'closed'
4635 elif status.find('open') != -1 or status == '1':
4636 return 'open'
4637 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004638 return 'unset'
4639
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004640
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004641def GetTreeStatusReason():
4642 """Fetches the tree status from a json url and returns the message
4643 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004644 url = settings.GetTreeStatusUrl()
4645 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004646 connection = urllib2.urlopen(json_url)
4647 status = json.loads(connection.read())
4648 connection.close()
4649 return status['message']
4650
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004651
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004652def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004653 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004654 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004655 status = GetTreeStatus()
4656 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004657 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004658 return 2
4659
vapiera7fbd5a2016-06-16 09:17:49 -07004660 print('The tree is %s' % status)
4661 print()
4662 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004663 if status != 'open':
4664 return 1
4665 return 0
4666
4667
maruel@chromium.org15192402012-09-06 12:38:29 +00004668def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004669 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004670 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004671 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004672 '-b', '--bot', action='append',
4673 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4674 'times to specify multiple builders. ex: '
4675 '"-b win_rel -b win_layout". See '
4676 'the try server waterfall for the builders name and the tests '
4677 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004678 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004679 '-B', '--bucket', default='',
4680 help=('Buildbucket bucket to send the try requests.'))
4681 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004682 '-m', '--master', default='',
4683 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004684 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004685 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004686 help='Revision to use for the try job; default: the revision will '
4687 'be determined by the try recipe that builder runs, which usually '
4688 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004689 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004690 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004691 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004692 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004693 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004694 '--project',
4695 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004696 'in recipe to determine to which repository or directory to '
4697 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004698 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004699 '-p', '--property', dest='properties', action='append', default=[],
4700 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004701 'key2=value2 etc. The value will be treated as '
4702 'json if decodable, or as string otherwise. '
4703 'NOTE: using this may make your try job not usable for CQ, '
4704 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004705 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004706 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4707 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004708 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004709 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004710 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004711 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004712
machenbach@chromium.org45453142015-09-15 08:45:22 +00004713 # Make sure that all properties are prop=value pairs.
4714 bad_params = [x for x in options.properties if '=' not in x]
4715 if bad_params:
4716 parser.error('Got properties with missing "=": %s' % bad_params)
4717
maruel@chromium.org15192402012-09-06 12:38:29 +00004718 if args:
4719 parser.error('Unknown arguments: %s' % args)
4720
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004721 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004722 if not cl.GetIssue():
4723 parser.error('Need to upload first')
4724
tandriie113dfd2016-10-11 10:20:12 -07004725 error_message = cl.CannotTriggerTryJobReason()
4726 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004727 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004728
borenet6c0efe62016-10-19 08:13:29 -07004729 if options.bucket and options.master:
4730 parser.error('Only one of --bucket and --master may be used.')
4731
qyearsley1fdfcb62016-10-24 13:22:03 -07004732 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004733
qyearsleydd49f942016-10-28 11:57:22 -07004734 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4735 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004736 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004737 if options.verbose:
4738 print('git cl try with no bots now defaults to CQ Dry Run.')
4739 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004740
borenet6c0efe62016-10-19 08:13:29 -07004741 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004742 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004743 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004744 'of bot requires an initial job from a parent (usually a builder). '
4745 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004746 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004747 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004748
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004749 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004750 # TODO(tandrii): Checking local patchset against remote patchset is only
4751 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4752 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004753 print('Warning: Codereview server has newer patchsets (%s) than most '
4754 'recent upload from local checkout (%s). Did a previous upload '
4755 'fail?\n'
4756 'By default, git cl try uses the latest patchset from '
4757 'codereview, continuing to use patchset %s.\n' %
4758 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004759
tandrii568043b2016-10-11 07:49:18 -07004760 try:
borenet6c0efe62016-10-19 08:13:29 -07004761 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4762 patchset)
tandrii568043b2016-10-11 07:49:18 -07004763 except BuildbucketResponseException as ex:
4764 print('ERROR: %s' % ex)
4765 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004766 return 0
4767
4768
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004769def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004770 """Prints info about try jobs associated with current CL."""
4771 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004772 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004773 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004774 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004775 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004776 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004777 '--color', action='store_true', default=setup_color.IS_TTY,
4778 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004779 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004780 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4781 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004782 group.add_option(
4783 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004784 parser.add_option_group(group)
4785 auth.add_auth_options(parser)
4786 options, args = parser.parse_args(args)
4787 if args:
4788 parser.error('Unrecognized args: %s' % ' '.join(args))
4789
4790 auth_config = auth.extract_auth_config_from_options(options)
4791 cl = Changelist(auth_config=auth_config)
4792 if not cl.GetIssue():
4793 parser.error('Need to upload first')
4794
tandrii221ab252016-10-06 08:12:04 -07004795 patchset = options.patchset
4796 if not patchset:
4797 patchset = cl.GetMostRecentPatchset()
4798 if not patchset:
4799 parser.error('Codereview doesn\'t know about issue %s. '
4800 'No access to issue or wrong issue number?\n'
4801 'Either upload first, or pass --patchset explicitely' %
4802 cl.GetIssue())
4803
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004804 # TODO(tandrii): Checking local patchset against remote patchset is only
4805 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4806 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004807 print('Warning: Codereview server has newer patchsets (%s) than most '
4808 'recent upload from local checkout (%s). Did a previous upload '
4809 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004810 'By default, git cl try-results uses the latest patchset from '
4811 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004812 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004813 try:
tandrii221ab252016-10-06 08:12:04 -07004814 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004815 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004816 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004817 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004818 if options.json:
4819 write_try_results_json(options.json, jobs)
4820 else:
4821 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004822 return 0
4823
4824
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004825@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004826def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004827 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004828 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004829 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004830 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004832 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004833 if args:
4834 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004835 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004836 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004837 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004838 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004839
4840 # Clear configured merge-base, if there is one.
4841 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004842 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004843 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004844 return 0
4845
4846
thestig@chromium.org00858c82013-12-02 23:08:03 +00004847def CMDweb(parser, args):
4848 """Opens the current CL in the web browser."""
4849 _, args = parser.parse_args(args)
4850 if args:
4851 parser.error('Unrecognized args: %s' % ' '.join(args))
4852
4853 issue_url = Changelist().GetIssueURL()
4854 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004855 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004856 return 1
4857
4858 webbrowser.open(issue_url)
4859 return 0
4860
4861
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004862def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004863 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004864 parser.add_option('-d', '--dry-run', action='store_true',
4865 help='trigger in dry run mode')
4866 parser.add_option('-c', '--clear', action='store_true',
4867 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004868 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004869 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004870 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004871 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004872 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004873 if args:
4874 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004875 if options.dry_run and options.clear:
4876 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4877
iannuccie53c9352016-08-17 14:40:40 -07004878 cl = Changelist(auth_config=auth_config, issue=options.issue,
4879 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004880 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004881 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004882 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004883 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004884 state = _CQState.DRY_RUN
4885 else:
4886 state = _CQState.COMMIT
4887 if not cl.GetIssue():
4888 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004889 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004890 return 0
4891
4892
groby@chromium.org411034a2013-02-26 15:12:01 +00004893def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004894 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004895 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004896 auth.add_auth_options(parser)
4897 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004898 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004899 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004900 if args:
4901 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004902 cl = Changelist(auth_config=auth_config, issue=options.issue,
4903 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004904 # Ensure there actually is an issue to close.
4905 cl.GetDescription()
4906 cl.CloseIssue()
4907 return 0
4908
4909
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004910def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004911 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004912 parser.add_option(
4913 '--stat',
4914 action='store_true',
4915 dest='stat',
4916 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004917 auth.add_auth_options(parser)
4918 options, args = parser.parse_args(args)
4919 auth_config = auth.extract_auth_config_from_options(options)
4920 if args:
4921 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004922
4923 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004924 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004925 # Staged changes would be committed along with the patch from last
4926 # upload, hence counted toward the "last upload" side in the final
4927 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004928 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004929 return 1
4930
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004931 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004932 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004933 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004934 if not issue:
4935 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004936 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004937 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004938
4939 # Create a new branch based on the merge-base
4940 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004941 # Clear cached branch in cl object, to avoid overwriting original CL branch
4942 # properties.
4943 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004944 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004945 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004946 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004947 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004948 return rtn
4949
wychen@chromium.org06928532015-02-03 02:11:29 +00004950 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004951 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004952 cmd = ['git', 'diff']
4953 if options.stat:
4954 cmd.append('--stat')
4955 cmd.extend([TMP_BRANCH, branch, '--'])
4956 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004957 finally:
4958 RunGit(['checkout', '-q', branch])
4959 RunGit(['branch', '-D', TMP_BRANCH])
4960
4961 return 0
4962
4963
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004964def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004965 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004966 parser.add_option(
4967 '--no-color',
4968 action='store_true',
4969 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004970 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004971 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004972 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004973
4974 author = RunGit(['config', 'user.email']).strip() or None
4975
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004976 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004977
4978 if args:
4979 if len(args) > 1:
4980 parser.error('Unknown args')
4981 base_branch = args[0]
4982 else:
4983 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004984 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004985
4986 change = cl.GetChange(base_branch, None)
4987 return owners_finder.OwnersFinder(
4988 [f.LocalPath() for f in
4989 cl.GetChange(base_branch, None).AffectedFiles()],
4990 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004991 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004992 disable_color=options.no_color).run()
4993
4994
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004995def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004996 """Generates a diff command."""
4997 # Generate diff for the current branch's changes.
4998 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004999 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005000
5001 if args:
5002 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005003 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005004 diff_cmd.append(arg)
5005 else:
5006 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005007
5008 return diff_cmd
5009
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005010
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005011def MatchingFileType(file_name, extensions):
5012 """Returns true if the file name ends with one of the given extensions."""
5013 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005014
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005015
enne@chromium.org555cfe42014-01-29 18:21:39 +00005016@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005017def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005018 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005019 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005020 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005021 parser.add_option('--full', action='store_true',
5022 help='Reformat the full content of all touched files')
5023 parser.add_option('--dry-run', action='store_true',
5024 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005025 parser.add_option('--python', action='store_true',
5026 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005027 parser.add_option('--js', action='store_true',
5028 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005029 parser.add_option('--diff', action='store_true',
5030 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005031 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005032
Daniel Chengc55eecf2016-12-30 03:11:02 -08005033 # Normalize any remaining args against the current path, so paths relative to
5034 # the current directory are still resolved as expected.
5035 args = [os.path.join(os.getcwd(), arg) for arg in args]
5036
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005037 # git diff generates paths against the root of the repository. Change
5038 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005039 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005040 if rel_base_path:
5041 os.chdir(rel_base_path)
5042
digit@chromium.org29e47272013-05-17 17:01:46 +00005043 # Grab the merge-base commit, i.e. the upstream commit of the current
5044 # branch when it was created or the last time it was rebased. This is
5045 # to cover the case where the user may have called "git fetch origin",
5046 # moving the origin branch to a newer commit, but hasn't rebased yet.
5047 upstream_commit = None
5048 cl = Changelist()
5049 upstream_branch = cl.GetUpstreamBranch()
5050 if upstream_branch:
5051 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5052 upstream_commit = upstream_commit.strip()
5053
5054 if not upstream_commit:
5055 DieWithError('Could not find base commit for this branch. '
5056 'Are you in detached state?')
5057
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005058 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5059 diff_output = RunGit(changed_files_cmd)
5060 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005061 # Filter out files deleted by this CL
5062 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005063
Christopher Lamc5ba6922017-01-24 11:19:14 +11005064 if opts.js:
5065 CLANG_EXTS.append('.js')
5066
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005067 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5068 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5069 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005070 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005071
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005072 top_dir = os.path.normpath(
5073 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5074
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005075 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5076 # formatted. This is used to block during the presubmit.
5077 return_value = 0
5078
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005079 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005080 # Locate the clang-format binary in the checkout
5081 try:
5082 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005083 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005084 DieWithError(e)
5085
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005086 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005087 cmd = [clang_format_tool]
5088 if not opts.dry_run and not opts.diff:
5089 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005090 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005091 if opts.diff:
5092 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005093 else:
5094 env = os.environ.copy()
5095 env['PATH'] = str(os.path.dirname(clang_format_tool))
5096 try:
5097 script = clang_format.FindClangFormatScriptInChromiumTree(
5098 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005099 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005100 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005101
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005102 cmd = [sys.executable, script, '-p0']
5103 if not opts.dry_run and not opts.diff:
5104 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005105
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005106 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5107 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005108
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005109 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5110 if opts.diff:
5111 sys.stdout.write(stdout)
5112 if opts.dry_run and len(stdout) > 0:
5113 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005114
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005115 # Similar code to above, but using yapf on .py files rather than clang-format
5116 # on C/C++ files
5117 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005118 yapf_tool = gclient_utils.FindExecutable('yapf')
5119 if yapf_tool is None:
5120 DieWithError('yapf not found in PATH')
5121
5122 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005123 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005124 cmd = [yapf_tool]
5125 if not opts.dry_run and not opts.diff:
5126 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005127 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005128 if opts.diff:
5129 sys.stdout.write(stdout)
5130 else:
5131 # TODO(sbc): yapf --lines mode still has some issues.
5132 # https://github.com/google/yapf/issues/154
5133 DieWithError('--python currently only works with --full')
5134
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005135 # Dart's formatter does not have the nice property of only operating on
5136 # modified chunks, so hard code full.
5137 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005138 try:
5139 command = [dart_format.FindDartFmtToolInChromiumTree()]
5140 if not opts.dry_run and not opts.diff:
5141 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005142 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005143
ppi@chromium.org6593d932016-03-03 15:41:15 +00005144 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005145 if opts.dry_run and stdout:
5146 return_value = 2
5147 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005148 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5149 'found in this checkout. Files in other languages are still '
5150 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005151
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005152 # Format GN build files. Always run on full build files for canonical form.
5153 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005154 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005155 if opts.dry_run or opts.diff:
5156 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005157 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005158 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5159 shell=sys.platform == 'win32',
5160 cwd=top_dir)
5161 if opts.dry_run and gn_ret == 2:
5162 return_value = 2 # Not formatted.
5163 elif opts.diff and gn_ret == 2:
5164 # TODO this should compute and print the actual diff.
5165 print("This change has GN build file diff for " + gn_diff_file)
5166 elif gn_ret != 0:
5167 # For non-dry run cases (and non-2 return values for dry-run), a
5168 # nonzero error code indicates a failure, probably because the file
5169 # doesn't parse.
5170 DieWithError("gn format failed on " + gn_diff_file +
5171 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005172
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005173 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005174
5175
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005176@subcommand.usage('<codereview url or issue id>')
5177def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005178 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005179 _, args = parser.parse_args(args)
5180
5181 if len(args) != 1:
5182 parser.print_help()
5183 return 1
5184
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005185 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005186 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005187 parser.print_help()
5188 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005189 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005190
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005191 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005192 output = RunGit(['config', '--local', '--get-regexp',
5193 r'branch\..*\.%s' % issueprefix],
5194 error_ok=True)
5195 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005196 if issue == target_issue:
5197 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005198
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005199 branches = []
5200 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005201 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005202 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005203 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005204 return 1
5205 if len(branches) == 1:
5206 RunGit(['checkout', branches[0]])
5207 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005208 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005209 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005210 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005211 which = raw_input('Choose by index: ')
5212 try:
5213 RunGit(['checkout', branches[int(which)]])
5214 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005215 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005216 return 1
5217
5218 return 0
5219
5220
maruel@chromium.org29404b52014-09-08 22:58:00 +00005221def CMDlol(parser, args):
5222 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005223 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005224 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5225 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5226 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005227 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005228 return 0
5229
5230
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005231class OptionParser(optparse.OptionParser):
5232 """Creates the option parse and add --verbose support."""
5233 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005234 optparse.OptionParser.__init__(
5235 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005236 self.add_option(
5237 '-v', '--verbose', action='count', default=0,
5238 help='Use 2 times for more debugging info')
5239
5240 def parse_args(self, args=None, values=None):
5241 options, args = optparse.OptionParser.parse_args(self, args, values)
5242 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005243 logging.basicConfig(
5244 level=levels[min(options.verbose, len(levels) - 1)],
5245 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5246 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005247 return options, args
5248
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005249
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005250def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005251 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005252 print('\nYour python version %s is unsupported, please upgrade.\n' %
5253 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005254 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005255
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005256 # Reload settings.
5257 global settings
5258 settings = Settings()
5259
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005260 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005261 dispatcher = subcommand.CommandDispatcher(__name__)
5262 try:
5263 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005264 except auth.AuthenticationError as e:
5265 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005266 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005267 if e.code != 500:
5268 raise
5269 DieWithError(
5270 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5271 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005272 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005273
5274
5275if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005276 # These affect sys.stdout so do it outside of main() to simplify mocks in
5277 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005278 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005279 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005280 try:
5281 sys.exit(main(sys.argv[1:]))
5282 except KeyboardInterrupt:
5283 sys.stderr.write('interrupted\n')
5284 sys.exit(1)