blob: ef702b95b66e89f4feceb9517fb264da3a0e9047 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010017import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000018import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000019import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000021import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import optparse
23import os
24import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000025import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080036 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
skobes6468b902016-10-24 08:45:10 -070044import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080067POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
borenet6c0efe62016-10-19 08:13:29 -070078# Buildbucket master name prefix.
79MASTER_PREFIX = 'master.'
80
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000081# Shortcut since it quickly becomes redundant.
82Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000083
maruel@chromium.orgddd59412011-11-30 14:20:38 +000084# Initialized in main()
85settings = None
86
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010087# Used by tests/git_cl_test.py to add extra logging.
88# Inside the weirdly failing test, add this:
89# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
90# And scroll up to see the strack trace printed.
91_IS_BEING_TESTED = False
92
maruel@chromium.orgddd59412011-11-30 14:20:38 +000093
Christopher Lamf732cd52017-01-24 12:40:11 +110094def DieWithError(message, change_desc=None):
95 if change_desc:
96 SaveDescriptionBackup(change_desc)
97
vapiera7fbd5a2016-06-16 09:17:49 -070098 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 sys.exit(1)
100
101
Christopher Lamf732cd52017-01-24 12:40:11 +1100102def SaveDescriptionBackup(change_desc):
103 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
104 print('\nError after CL description prompt -- saving description to %s\n' %
105 backup_path)
106 backup_file = open(backup_path, 'w')
107 backup_file.write(change_desc.description)
108 backup_file.close()
109
110
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000111def GetNoGitPagerEnv():
112 env = os.environ.copy()
113 # 'cat' is a magical git string that disables pagers on all platforms.
114 env['GIT_PAGER'] = 'cat'
115 return env
116
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000117
bsep@chromium.org627d9002016-04-29 00:00:52 +0000118def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000119 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000120 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000121 except subprocess2.CalledProcessError as e:
122 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000123 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000124 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000125 'Command "%s" failed.\n%s' % (
126 ' '.join(args), error_message or e.stdout or ''))
127 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128
129
130def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000131 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000132 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000135def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700137 if suppress_stderr:
138 stderr = subprocess2.VOID
139 else:
140 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000141 try:
tandrii5d48c322016-08-18 16:19:37 -0700142 (out, _), code = subprocess2.communicate(['git'] + args,
143 env=GetNoGitPagerEnv(),
144 stdout=subprocess2.PIPE,
145 stderr=stderr)
146 return code, out
147 except subprocess2.CalledProcessError as e:
148 logging.debug('Failed running %s', args)
149 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000150
151
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000152def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000153 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000154 return RunGitWithCode(args, suppress_stderr=True)[1]
155
156
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000157def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000158 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000159 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000160 return (version.startswith(prefix) and
161 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162
163
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000164def BranchExists(branch):
165 """Return True if specified branch exists."""
166 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
167 suppress_stderr=True)
168 return not code
169
170
tandrii2a16b952016-10-19 07:09:44 -0700171def time_sleep(seconds):
172 # Use this so that it can be mocked in tests without interfering with python
173 # system machinery.
174 import time # Local import to discourage others from importing time globally.
175 return time.sleep(seconds)
176
177
maruel@chromium.org90541732011-04-01 17:54:18 +0000178def ask_for_data(prompt):
179 try:
180 return raw_input(prompt)
181 except KeyboardInterrupt:
182 # Hide the exception.
183 sys.exit(1)
184
185
tandrii5d48c322016-08-18 16:19:37 -0700186def _git_branch_config_key(branch, key):
187 """Helper method to return Git config key for a branch."""
188 assert branch, 'branch name is required to set git config for it'
189 return 'branch.%s.%s' % (branch, key)
190
191
192def _git_get_branch_config_value(key, default=None, value_type=str,
193 branch=False):
194 """Returns git config value of given or current branch if any.
195
196 Returns default in all other cases.
197 """
198 assert value_type in (int, str, bool)
199 if branch is False: # Distinguishing default arg value from None.
200 branch = GetCurrentBranch()
201
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000202 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700203 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000204
tandrii5d48c322016-08-18 16:19:37 -0700205 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700206 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700207 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700208 # git config also has --int, but apparently git config suffers from integer
209 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700210 args.append(_git_branch_config_key(branch, key))
211 code, out = RunGitWithCode(args)
212 if code == 0:
213 value = out.strip()
214 if value_type == int:
215 return int(value)
216 if value_type == bool:
217 return bool(value.lower() == 'true')
218 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000219 return default
220
221
tandrii5d48c322016-08-18 16:19:37 -0700222def _git_set_branch_config_value(key, value, branch=None, **kwargs):
223 """Sets the value or unsets if it's None of a git branch config.
224
225 Valid, though not necessarily existing, branch must be provided,
226 otherwise currently checked out branch is used.
227 """
228 if not branch:
229 branch = GetCurrentBranch()
230 assert branch, 'a branch name OR currently checked out branch is required'
231 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700232 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700233 if value is None:
234 args.append('--unset')
235 elif isinstance(value, bool):
236 args.append('--bool')
237 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700238 else:
tandrii33a46ff2016-08-23 05:53:40 -0700239 # git config also has --int, but apparently git config suffers from integer
240 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700241 value = str(value)
242 args.append(_git_branch_config_key(branch, key))
243 if value is not None:
244 args.append(value)
245 RunGit(args, **kwargs)
246
247
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100248def _get_committer_timestamp(commit):
249 """Returns unix timestamp as integer of a committer in a commit.
250
251 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
252 """
253 # Git also stores timezone offset, but it only affects visual display,
254 # actual point in time is defined by this timestamp only.
255 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
256
257
258def _git_amend_head(message, committer_timestamp):
259 """Amends commit with new message and desired committer_timestamp.
260
261 Sets committer timezone to UTC.
262 """
263 env = os.environ.copy()
264 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
265 return RunGit(['commit', '--amend', '-m', message], env=env)
266
267
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000268def add_git_similarity(parser):
269 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700270 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000271 help='Sets the percentage that a pair of files need to match in order to'
272 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000273 parser.add_option(
274 '--find-copies', action='store_true',
275 help='Allows git to look for copies.')
276 parser.add_option(
277 '--no-find-copies', action='store_false', dest='find_copies',
278 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000279
280 old_parser_args = parser.parse_args
281 def Parse(args):
282 options, args = old_parser_args(args)
283
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000284 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700285 options.similarity = _git_get_branch_config_value(
286 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000287 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000288 print('Note: Saving similarity of %d%% in git config.'
289 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700290 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000291
iannucci@chromium.org79540052012-10-19 23:15:26 +0000292 options.similarity = max(0, min(options.similarity, 100))
293
294 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700295 options.find_copies = _git_get_branch_config_value(
296 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000297 else:
tandrii5d48c322016-08-18 16:19:37 -0700298 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000299
300 print('Using %d%% similarity for rename/copy detection. '
301 'Override with --similarity.' % options.similarity)
302
303 return options, args
304 parser.parse_args = Parse
305
306
machenbach@chromium.org45453142015-09-15 08:45:22 +0000307def _get_properties_from_options(options):
308 properties = dict(x.split('=', 1) for x in options.properties)
309 for key, val in properties.iteritems():
310 try:
311 properties[key] = json.loads(val)
312 except ValueError:
313 pass # If a value couldn't be evaluated, treat it as a string.
314 return properties
315
316
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000317def _prefix_master(master):
318 """Convert user-specified master name to full master name.
319
320 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
321 name, while the developers always use shortened master name
322 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
323 function does the conversion for buildbucket migration.
324 """
borenet6c0efe62016-10-19 08:13:29 -0700325 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000326 return master
borenet6c0efe62016-10-19 08:13:29 -0700327 return '%s%s' % (MASTER_PREFIX, master)
328
329
330def _unprefix_master(bucket):
331 """Convert bucket name to shortened master name.
332
333 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
334 name, while the developers always use shortened master name
335 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
336 function does the conversion for buildbucket migration.
337 """
338 if bucket.startswith(MASTER_PREFIX):
339 return bucket[len(MASTER_PREFIX):]
340 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000341
342
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000343def _buildbucket_retry(operation_name, http, *args, **kwargs):
344 """Retries requests to buildbucket service and returns parsed json content."""
345 try_count = 0
346 while True:
347 response, content = http.request(*args, **kwargs)
348 try:
349 content_json = json.loads(content)
350 except ValueError:
351 content_json = None
352
353 # Buildbucket could return an error even if status==200.
354 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000355 error = content_json.get('error')
356 if error.get('code') == 403:
357 raise BuildbucketResponseException(
358 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000360 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000361 raise BuildbucketResponseException(msg)
362
363 if response.status == 200:
364 if not content_json:
365 raise BuildbucketResponseException(
366 'Buildbucket returns invalid json content: %s.\n'
367 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
368 content)
369 return content_json
370 if response.status < 500 or try_count >= 2:
371 raise httplib2.HttpLib2Error(content)
372
373 # status >= 500 means transient failures.
374 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700375 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000376 try_count += 1
377 assert False, 'unreachable'
378
379
qyearsley1fdfcb62016-10-24 13:22:03 -0700380def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700381 """Returns a dict mapping bucket names to builders and tests,
382 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700383 """
qyearsleydd49f942016-10-28 11:57:22 -0700384 # If no bots are listed, we try to get a set of builders and tests based
385 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700386 if not options.bot:
387 change = changelist.GetChange(
388 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700389 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700390 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700391 change=change,
392 changed_files=change.LocalPaths(),
393 repository_root=settings.GetRoot(),
394 default_presubmit=None,
395 project=None,
396 verbose=options.verbose,
397 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700398 if masters is None:
399 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100400 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700401
qyearsley1fdfcb62016-10-24 13:22:03 -0700402 if options.bucket:
403 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700404 if options.master:
405 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
qyearsleydd49f942016-10-28 11:57:22 -0700407 # If bots are listed but no master or bucket, then we need to find out
408 # the corresponding master for each bot.
409 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
410 if error_message:
411 option_parser.error(
412 'Tryserver master cannot be found because: %s\n'
413 'Please manually specify the tryserver master, e.g. '
414 '"-m tryserver.chromium.linux".' % error_message)
415 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700416
417
qyearsley123a4682016-10-26 09:12:17 -0700418def _get_bucket_map_for_builders(builders):
419 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700420 map_url = 'https://builders-map.appspot.com/'
421 try:
qyearsley123a4682016-10-26 09:12:17 -0700422 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700423 except urllib2.URLError as e:
424 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
425 (map_url, e))
426 except ValueError as e:
427 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700428 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700429 return None, 'Failed to build master map.'
430
qyearsley123a4682016-10-26 09:12:17 -0700431 bucket_map = {}
432 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700433 masters = builders_map.get(builder, [])
434 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700435 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700436 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700437 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700438 (builder, masters))
439 bucket = _prefix_master(masters[0])
440 bucket_map.setdefault(bucket, {})[builder] = []
441
442 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700443
444
borenet6c0efe62016-10-19 08:13:29 -0700445def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700446 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700447 """Sends a request to Buildbucket to trigger try jobs for a changelist.
448
449 Args:
450 auth_config: AuthConfig for Rietveld.
451 changelist: Changelist that the try jobs are associated with.
452 buckets: A nested dict mapping bucket names to builders to tests.
453 options: Command-line options.
454 """
tandriide281ae2016-10-12 06:02:30 -0700455 assert changelist.GetIssue(), 'CL must be uploaded first'
456 codereview_url = changelist.GetCodereviewServer()
457 assert codereview_url, 'CL must be uploaded first'
458 patchset = patchset or changelist.GetMostRecentPatchset()
459 assert patchset, 'CL must be uploaded first'
460
461 codereview_host = urlparse.urlparse(codereview_url).hostname
462 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000463 http = authenticator.authorize(httplib2.Http())
464 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700465
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000466 buildbucket_put_url = (
467 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000468 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700469 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
470 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
471 hostname=codereview_host,
472 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000473 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700474
475 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
476 shared_parameters_properties['category'] = category
477 if options.clobber:
478 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700479 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700480 if extra_properties:
481 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000482
483 batch_req_body = {'builds': []}
484 print_text = []
485 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700486 for bucket, builders_and_tests in sorted(buckets.iteritems()):
487 print_text.append('Bucket: %s' % bucket)
488 master = None
489 if bucket.startswith(MASTER_PREFIX):
490 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000491 for builder, tests in sorted(builders_and_tests.iteritems()):
492 print_text.append(' %s: %s' % (builder, tests))
493 parameters = {
494 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000495 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100496 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000497 'revision': options.revision,
498 }],
tandrii8c5a3532016-11-04 07:52:02 -0700499 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000500 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000501 if 'presubmit' in builder.lower():
502 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000503 if tests:
504 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700505
506 tags = [
507 'builder:%s' % builder,
508 'buildset:%s' % buildset,
509 'user_agent:git_cl_try',
510 ]
511 if master:
512 parameters['properties']['master'] = master
513 tags.append('master:%s' % master)
514
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000515 batch_req_body['builds'].append(
516 {
517 'bucket': bucket,
518 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000519 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700520 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000521 }
522 )
523
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000524 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700525 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000526 http,
527 buildbucket_put_url,
528 'PUT',
529 body=json.dumps(batch_req_body),
530 headers={'Content-Type': 'application/json'}
531 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000532 print_text.append('To see results here, run: git cl try-results')
533 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700534 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000535
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000536
tandrii221ab252016-10-06 08:12:04 -0700537def fetch_try_jobs(auth_config, changelist, buildbucket_host,
538 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700539 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000540
qyearsley53f48a12016-09-01 10:45:13 -0700541 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000542 """
tandrii221ab252016-10-06 08:12:04 -0700543 assert buildbucket_host
544 assert changelist.GetIssue(), 'CL must be uploaded first'
545 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
546 patchset = patchset or changelist.GetMostRecentPatchset()
547 assert patchset, 'CL must be uploaded first'
548
549 codereview_url = changelist.GetCodereviewServer()
550 codereview_host = urlparse.urlparse(codereview_url).hostname
551 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 if authenticator.has_cached_credentials():
553 http = authenticator.authorize(httplib2.Http())
554 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700555 print('Warning: Some results might be missing because %s' %
556 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700557 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 http = httplib2.Http()
559
560 http.force_exception_to_status_code = True
561
tandrii221ab252016-10-06 08:12:04 -0700562 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
563 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
564 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700566 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567 params = {'tag': 'buildset:%s' % buildset}
568
569 builds = {}
570 while True:
571 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700572 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700574 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000575 for build in content.get('builds', []):
576 builds[build['id']] = build
577 if 'next_cursor' in content:
578 params['start_cursor'] = content['next_cursor']
579 else:
580 break
581 return builds
582
583
qyearsleyeab3c042016-08-24 09:18:28 -0700584def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 """Prints nicely result of fetch_try_jobs."""
586 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700587 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000588 return
589
590 # Make a copy, because we'll be modifying builds dictionary.
591 builds = builds.copy()
592 builder_names_cache = {}
593
594 def get_builder(b):
595 try:
596 return builder_names_cache[b['id']]
597 except KeyError:
598 try:
599 parameters = json.loads(b['parameters_json'])
600 name = parameters['builder_name']
601 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700602 print('WARNING: failed to get builder name for build %s: %s' % (
603 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000604 name = None
605 builder_names_cache[b['id']] = name
606 return name
607
608 def get_bucket(b):
609 bucket = b['bucket']
610 if bucket.startswith('master.'):
611 return bucket[len('master.'):]
612 return bucket
613
614 if options.print_master:
615 name_fmt = '%%-%ds %%-%ds' % (
616 max(len(str(get_bucket(b))) for b in builds.itervalues()),
617 max(len(str(get_builder(b))) for b in builds.itervalues()))
618 def get_name(b):
619 return name_fmt % (get_bucket(b), get_builder(b))
620 else:
621 name_fmt = '%%-%ds' % (
622 max(len(str(get_builder(b))) for b in builds.itervalues()))
623 def get_name(b):
624 return name_fmt % get_builder(b)
625
626 def sort_key(b):
627 return b['status'], b.get('result'), get_name(b), b.get('url')
628
629 def pop(title, f, color=None, **kwargs):
630 """Pop matching builds from `builds` dict and print them."""
631
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000632 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000633 colorize = str
634 else:
635 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
636
637 result = []
638 for b in builds.values():
639 if all(b.get(k) == v for k, v in kwargs.iteritems()):
640 builds.pop(b['id'])
641 result.append(b)
642 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700643 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000644 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700645 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000646
647 total = len(builds)
648 pop(status='COMPLETED', result='SUCCESS',
649 title='Successes:', color=Fore.GREEN,
650 f=lambda b: (get_name(b), b.get('url')))
651 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
652 title='Infra Failures:', color=Fore.MAGENTA,
653 f=lambda b: (get_name(b), b.get('url')))
654 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
655 title='Failures:', color=Fore.RED,
656 f=lambda b: (get_name(b), b.get('url')))
657 pop(status='COMPLETED', result='CANCELED',
658 title='Canceled:', color=Fore.MAGENTA,
659 f=lambda b: (get_name(b),))
660 pop(status='COMPLETED', result='FAILURE',
661 failure_reason='INVALID_BUILD_DEFINITION',
662 title='Wrong master/builder name:', color=Fore.MAGENTA,
663 f=lambda b: (get_name(b),))
664 pop(status='COMPLETED', result='FAILURE',
665 title='Other failures:',
666 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
667 pop(status='COMPLETED',
668 title='Other finished:',
669 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
670 pop(status='STARTED',
671 title='Started:', color=Fore.YELLOW,
672 f=lambda b: (get_name(b), b.get('url')))
673 pop(status='SCHEDULED',
674 title='Scheduled:',
675 f=lambda b: (get_name(b), 'id=%s' % b['id']))
676 # The last section is just in case buildbucket API changes OR there is a bug.
677 pop(title='Other:',
678 f=lambda b: (get_name(b), 'id=%s' % b['id']))
679 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700680 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000681
682
qyearsley53f48a12016-09-01 10:45:13 -0700683def write_try_results_json(output_file, builds):
684 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
685
686 The input |builds| dict is assumed to be generated by Buildbucket.
687 Buildbucket documentation: http://goo.gl/G0s101
688 """
689
690 def convert_build_dict(build):
691 return {
692 'buildbucket_id': build.get('id'),
693 'status': build.get('status'),
694 'result': build.get('result'),
695 'bucket': build.get('bucket'),
696 'builder_name': json.loads(
697 build.get('parameters_json', '{}')).get('builder_name'),
698 'failure_reason': build.get('failure_reason'),
699 'url': build.get('url'),
700 }
701
702 converted = []
703 for _, build in sorted(builds.items()):
704 converted.append(convert_build_dict(build))
705 write_json(output_file, converted)
706
707
iannucci@chromium.org79540052012-10-19 23:15:26 +0000708def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000709 """Prints statistics about the change to the user."""
710 # --no-ext-diff is broken in some versions of Git, so try to work around
711 # this by overriding the environment (but there is still a problem if the
712 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000713 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000714 if 'GIT_EXTERNAL_DIFF' in env:
715 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000716
717 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800718 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000719 else:
720 similarity_options = ['-M%s' % similarity]
721
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000722 try:
723 stdout = sys.stdout.fileno()
724 except AttributeError:
725 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000726 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000727 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000728 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000729 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000730
731
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000732class BuildbucketResponseException(Exception):
733 pass
734
735
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736class Settings(object):
737 def __init__(self):
738 self.default_server = None
739 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000740 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741 self.tree_status_url = None
742 self.viewvc_url = None
743 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000744 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000745 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000746 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000747 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000748 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000749 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750
751 def LazyUpdateIfNeeded(self):
752 """Updates the settings from a codereview.settings file, if available."""
753 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000754 # The only value that actually changes the behavior is
755 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000756 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000757 error_ok=True
758 ).strip().lower()
759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000761 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762 LoadCodereviewSettingsFromFile(cr_settings_file)
763 self.updated = True
764
765 def GetDefaultServerUrl(self, error_ok=False):
766 if not self.default_server:
767 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000768 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000769 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 if error_ok:
771 return self.default_server
772 if not self.default_server:
773 error_message = ('Could not find settings file. You must configure '
774 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000775 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000776 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777 return self.default_server
778
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000779 @staticmethod
780 def GetRelativeRoot():
781 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000782
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000784 if self.root is None:
785 self.root = os.path.abspath(self.GetRelativeRoot())
786 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000788 def GetGitMirror(self, remote='origin'):
789 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000790 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000791 if not os.path.isdir(local_url):
792 return None
793 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
794 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100795 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100796 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000797 if mirror.exists():
798 return mirror
799 return None
800
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 def GetTreeStatusUrl(self, error_ok=False):
802 if not self.tree_status_url:
803 error_message = ('You must configure your tree status URL by running '
804 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 self.tree_status_url = self._GetRietveldConfig(
806 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 return self.tree_status_url
808
809 def GetViewVCUrl(self):
810 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 return self.viewvc_url
813
rmistry@google.com90752582014-01-14 21:04:50 +0000814 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000815 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000816
rmistry@google.com78948ed2015-07-08 23:09:57 +0000817 def GetIsSkipDependencyUpload(self, branch_name):
818 """Returns true if specified branch should skip dep uploads."""
819 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
820 error_ok=True)
821
rmistry@google.com5626a922015-02-26 14:03:30 +0000822 def GetRunPostUploadHook(self):
823 run_post_upload_hook = self._GetRietveldConfig(
824 'run-post-upload-hook', error_ok=True)
825 return run_post_upload_hook == "True"
826
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000827 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000828 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000829
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000830 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000831 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000832
ukai@chromium.orge8077812012-02-03 03:41:46 +0000833 def GetIsGerrit(self):
834 """Return true if this repo is assosiated with gerrit code review system."""
835 if self.is_gerrit is None:
836 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
837 return self.is_gerrit
838
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000839 def GetSquashGerritUploads(self):
840 """Return true if uploads to Gerrit should be squashed by default."""
841 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700842 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
843 if self.squash_gerrit_uploads is None:
844 # Default is squash now (http://crbug.com/611892#c23).
845 self.squash_gerrit_uploads = not (
846 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
847 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000848 return self.squash_gerrit_uploads
849
tandriia60502f2016-06-20 02:01:53 -0700850 def GetSquashGerritUploadsOverride(self):
851 """Return True or False if codereview.settings should be overridden.
852
853 Returns None if no override has been defined.
854 """
855 # See also http://crbug.com/611892#c23
856 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
857 error_ok=True).strip()
858 if result == 'true':
859 return True
860 if result == 'false':
861 return False
862 return None
863
tandrii@chromium.org28253532016-04-14 13:46:56 +0000864 def GetGerritSkipEnsureAuthenticated(self):
865 """Return True if EnsureAuthenticated should not be done for Gerrit
866 uploads."""
867 if self.gerrit_skip_ensure_authenticated is None:
868 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000869 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000870 error_ok=True).strip() == 'true')
871 return self.gerrit_skip_ensure_authenticated
872
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000873 def GetGitEditor(self):
874 """Return the editor specified in the git config, or None if none is."""
875 if self.git_editor is None:
876 self.git_editor = self._GetConfig('core.editor', error_ok=True)
877 return self.git_editor or None
878
thestig@chromium.org44202a22014-03-11 19:22:18 +0000879 def GetLintRegex(self):
880 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
881 DEFAULT_LINT_REGEX)
882
883 def GetLintIgnoreRegex(self):
884 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
885 DEFAULT_LINT_IGNORE_REGEX)
886
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000887 def GetProject(self):
888 if not self.project:
889 self.project = self._GetRietveldConfig('project', error_ok=True)
890 return self.project
891
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000892 def _GetRietveldConfig(self, param, **kwargs):
893 return self._GetConfig('rietveld.' + param, **kwargs)
894
rmistry@google.com78948ed2015-07-08 23:09:57 +0000895 def _GetBranchConfig(self, branch_name, param, **kwargs):
896 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
897
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898 def _GetConfig(self, param, **kwargs):
899 self.LazyUpdateIfNeeded()
900 return RunGit(['config', param], **kwargs).strip()
901
902
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100903@contextlib.contextmanager
904def _get_gerrit_project_config_file(remote_url):
905 """Context manager to fetch and store Gerrit's project.config from
906 refs/meta/config branch and store it in temp file.
907
908 Provides a temporary filename or None if there was error.
909 """
910 error, _ = RunGitWithCode([
911 'fetch', remote_url,
912 '+refs/meta/config:refs/git_cl/meta/config'])
913 if error:
914 # Ref doesn't exist or isn't accessible to current user.
915 print('WARNING: failed to fetch project config for %s: %s' %
916 (remote_url, error))
917 yield None
918 return
919
920 error, project_config_data = RunGitWithCode(
921 ['show', 'refs/git_cl/meta/config:project.config'])
922 if error:
923 print('WARNING: project.config file not found')
924 yield None
925 return
926
927 with gclient_utils.temporary_directory() as tempdir:
928 project_config_file = os.path.join(tempdir, 'project.config')
929 gclient_utils.FileWrite(project_config_file, project_config_data)
930 yield project_config_file
931
932
933def _is_git_numberer_enabled(remote_url, remote_ref):
934 """Returns True if Git Numberer is enabled on this ref."""
935 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100936 KNOWN_PROJECTS_WHITELIST = [
937 'chromium/src',
938 'external/webrtc',
939 'v8/v8',
940 ]
941
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100942 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
943 url_parts = urlparse.urlparse(remote_url)
944 project_name = url_parts.path.lstrip('/').rstrip('git./')
945 for known in KNOWN_PROJECTS_WHITELIST:
946 if project_name.endswith(known):
947 break
948 else:
949 # Early exit to avoid extra fetches for repos that aren't using Git
950 # Numberer.
951 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100952
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100953 with _get_gerrit_project_config_file(remote_url) as project_config_file:
954 if project_config_file is None:
955 # Failed to fetch project.config, which shouldn't happen on open source
956 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100957 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100958 def get_opts(x):
959 code, out = RunGitWithCode(
960 ['config', '-f', project_config_file, '--get-all',
961 'plugin.git-numberer.validate-%s-refglob' % x])
962 if code == 0:
963 return out.strip().splitlines()
964 return []
965 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100966
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100967 logging.info('validator config enabled %s disabled %s refglobs for '
968 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000969
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100970 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100971 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100972 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100973 return True
974 return False
975
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100976 if match_refglobs(disabled):
977 return False
978 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100979
980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981def ShortBranchName(branch):
982 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000983 return branch.replace('refs/heads/', '', 1)
984
985
986def GetCurrentBranchRef():
987 """Returns branch ref (e.g., refs/heads/master) or None."""
988 return RunGit(['symbolic-ref', 'HEAD'],
989 stderr=subprocess2.VOID, error_ok=True).strip() or None
990
991
992def GetCurrentBranch():
993 """Returns current branch or None.
994
995 For refs/heads/* branches, returns just last part. For others, full ref.
996 """
997 branchref = GetCurrentBranchRef()
998 if branchref:
999 return ShortBranchName(branchref)
1000 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001
1002
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001003class _CQState(object):
1004 """Enum for states of CL with respect to Commit Queue."""
1005 NONE = 'none'
1006 DRY_RUN = 'dry_run'
1007 COMMIT = 'commit'
1008
1009 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1010
1011
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001012class _ParsedIssueNumberArgument(object):
1013 def __init__(self, issue=None, patchset=None, hostname=None):
1014 self.issue = issue
1015 self.patchset = patchset
1016 self.hostname = hostname
1017
1018 @property
1019 def valid(self):
1020 return self.issue is not None
1021
1022
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001023def ParseIssueNumberArgument(arg):
1024 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1025 fail_result = _ParsedIssueNumberArgument()
1026
1027 if arg.isdigit():
1028 return _ParsedIssueNumberArgument(issue=int(arg))
1029 if not arg.startswith('http'):
1030 return fail_result
1031 url = gclient_utils.UpgradeToHttps(arg)
1032 try:
1033 parsed_url = urlparse.urlparse(url)
1034 except ValueError:
1035 return fail_result
1036 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1037 tmp = cls.ParseIssueURL(parsed_url)
1038 if tmp is not None:
1039 return tmp
1040 return fail_result
1041
1042
Aaron Gablea45ee112016-11-22 15:14:38 -08001043class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001044 def __init__(self, issue, url):
1045 self.issue = issue
1046 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001047 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001048
1049 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001050 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001051 self.issue, self.url)
1052
1053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001055 """Changelist works with one changelist in local branch.
1056
1057 Supports two codereview backends: Rietveld or Gerrit, selected at object
1058 creation.
1059
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001060 Notes:
1061 * Not safe for concurrent multi-{thread,process} use.
1062 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001063 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001064 """
1065
1066 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1067 """Create a new ChangeList instance.
1068
1069 If issue is given, the codereview must be given too.
1070
1071 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1072 Otherwise, it's decided based on current configuration of the local branch,
1073 with default being 'rietveld' for backwards compatibility.
1074 See _load_codereview_impl for more details.
1075
1076 **kwargs will be passed directly to codereview implementation.
1077 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001079 global settings
1080 if not settings:
1081 # Happens when git_cl.py is used as a utility library.
1082 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001083
1084 if issue:
1085 assert codereview, 'codereview must be known, if issue is known'
1086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087 self.branchref = branchref
1088 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001089 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 self.branch = ShortBranchName(self.branchref)
1091 else:
1092 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001094 self.lookedup_issue = False
1095 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001096 self.has_description = False
1097 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001098 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001100 self.cc = None
1101 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001102 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001103
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001104 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001105 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001106 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001107 assert self._codereview_impl
1108 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001109
1110 def _load_codereview_impl(self, codereview=None, **kwargs):
1111 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001112 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1113 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1114 self._codereview = codereview
1115 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116 return
1117
1118 # Automatic selection based on issue number set for a current branch.
1119 # Rietveld takes precedence over Gerrit.
1120 assert not self.issue
1121 # Whether we find issue or not, we are doing the lookup.
1122 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001123 if self.GetBranch():
1124 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1125 issue = _git_get_branch_config_value(
1126 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1127 if issue:
1128 self._codereview = codereview
1129 self._codereview_impl = cls(self, **kwargs)
1130 self.issue = int(issue)
1131 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001132
1133 # No issue is set for this branch, so decide based on repo-wide settings.
1134 return self._load_codereview_impl(
1135 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1136 **kwargs)
1137
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001138 def IsGerrit(self):
1139 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001140
1141 def GetCCList(self):
1142 """Return the users cc'd on this CL.
1143
agable92bec4f2016-08-24 09:27:27 -07001144 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001145 """
1146 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001147 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001148 more_cc = ','.join(self.watchers)
1149 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1150 return self.cc
1151
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001152 def GetCCListWithoutDefault(self):
1153 """Return the users cc'd on this CL excluding default ones."""
1154 if self.cc is None:
1155 self.cc = ','.join(self.watchers)
1156 return self.cc
1157
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001158 def SetWatchers(self, watchers):
1159 """Set the list of email addresses that should be cc'd based on the changed
1160 files in this CL.
1161 """
1162 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163
1164 def GetBranch(self):
1165 """Returns the short branch name, e.g. 'master'."""
1166 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001167 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001168 if not branchref:
1169 return None
1170 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171 self.branch = ShortBranchName(self.branchref)
1172 return self.branch
1173
1174 def GetBranchRef(self):
1175 """Returns the full branch name, e.g. 'refs/heads/master'."""
1176 self.GetBranch() # Poke the lazy loader.
1177 return self.branchref
1178
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001179 def ClearBranch(self):
1180 """Clears cached branch data of this object."""
1181 self.branch = self.branchref = None
1182
tandrii5d48c322016-08-18 16:19:37 -07001183 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1184 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1185 kwargs['branch'] = self.GetBranch()
1186 return _git_get_branch_config_value(key, default, **kwargs)
1187
1188 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1189 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1190 assert self.GetBranch(), (
1191 'this CL must have an associated branch to %sset %s%s' %
1192 ('un' if value is None else '',
1193 key,
1194 '' if value is None else ' to %r' % value))
1195 kwargs['branch'] = self.GetBranch()
1196 return _git_set_branch_config_value(key, value, **kwargs)
1197
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001198 @staticmethod
1199 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001200 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 e.g. 'origin', 'refs/heads/master'
1202 """
1203 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001204 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1205
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001207 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001209 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1210 error_ok=True).strip()
1211 if upstream_branch:
1212 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001214 # Else, try to guess the origin remote.
1215 remote_branches = RunGit(['branch', '-r']).split()
1216 if 'origin/master' in remote_branches:
1217 # Fall back on origin/master if it exits.
1218 remote = 'origin'
1219 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001221 DieWithError(
1222 'Unable to determine default branch to diff against.\n'
1223 'Either pass complete "git diff"-style arguments, like\n'
1224 ' git cl upload origin/master\n'
1225 'or verify this branch is set up to track another \n'
1226 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227
1228 return remote, upstream_branch
1229
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001230 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001231 upstream_branch = self.GetUpstreamBranch()
1232 if not BranchExists(upstream_branch):
1233 DieWithError('The upstream for the current branch (%s) does not exist '
1234 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001235 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001236 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001237
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 def GetUpstreamBranch(self):
1239 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001240 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001242 upstream_branch = upstream_branch.replace('refs/heads/',
1243 'refs/remotes/%s/' % remote)
1244 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1245 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 self.upstream_branch = upstream_branch
1247 return self.upstream_branch
1248
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001249 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001250 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001251 remote, branch = None, self.GetBranch()
1252 seen_branches = set()
1253 while branch not in seen_branches:
1254 seen_branches.add(branch)
1255 remote, branch = self.FetchUpstreamTuple(branch)
1256 branch = ShortBranchName(branch)
1257 if remote != '.' or branch.startswith('refs/remotes'):
1258 break
1259 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001260 remotes = RunGit(['remote'], error_ok=True).split()
1261 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001262 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001263 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001264 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001265 logging.warn('Could not determine which remote this change is '
1266 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001267 else:
1268 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001269 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 branch = 'HEAD'
1271 if branch.startswith('refs/remotes'):
1272 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001273 elif branch.startswith('refs/branch-heads/'):
1274 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001275 else:
1276 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001277 return self._remote
1278
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001279 def GitSanityChecks(self, upstream_git_obj):
1280 """Checks git repo status and ensures diff is from local commits."""
1281
sbc@chromium.org79706062015-01-14 21:18:12 +00001282 if upstream_git_obj is None:
1283 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001284 print('ERROR: unable to determine current branch (detached HEAD?)',
1285 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001286 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001287 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001288 return False
1289
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 # Verify the commit we're diffing against is in our current branch.
1291 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1292 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1293 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001294 print('ERROR: %s is not in the current branch. You may need to rebase '
1295 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001296 return False
1297
1298 # List the commits inside the diff, and verify they are all local.
1299 commits_in_diff = RunGit(
1300 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1301 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1302 remote_branch = remote_branch.strip()
1303 if code != 0:
1304 _, remote_branch = self.GetRemoteBranch()
1305
1306 commits_in_remote = RunGit(
1307 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1308
1309 common_commits = set(commits_in_diff) & set(commits_in_remote)
1310 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001311 print('ERROR: Your diff contains %d commits already in %s.\n'
1312 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1313 'the diff. If you are using a custom git flow, you can override'
1314 ' the reference used for this check with "git config '
1315 'gitcl.remotebranch <git-ref>".' % (
1316 len(common_commits), remote_branch, upstream_git_obj),
1317 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001318 return False
1319 return True
1320
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001321 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001322 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001323
1324 Returns None if it is not set.
1325 """
tandrii5d48c322016-08-18 16:19:37 -07001326 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001327
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 def GetRemoteUrl(self):
1329 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1330
1331 Returns None if there is no remote.
1332 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001333 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001334 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1335
1336 # If URL is pointing to a local directory, it is probably a git cache.
1337 if os.path.isdir(url):
1338 url = RunGit(['config', 'remote.%s.url' % remote],
1339 error_ok=True,
1340 cwd=url).strip()
1341 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001343 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001344 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001345 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001346 self.issue = self._GitGetBranchConfigValue(
1347 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001348 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349 return self.issue
1350
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351 def GetIssueURL(self):
1352 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001353 issue = self.GetIssue()
1354 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001355 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001356 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357
1358 def GetDescription(self, pretty=False):
1359 if not self.has_description:
1360 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001361 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 self.has_description = True
1363 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001364 # Set width to 72 columns + 2 space indent.
1365 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001367 lines = self.description.splitlines()
1368 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 return self.description
1370
1371 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001372 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001373 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001374 self.patchset = self._GitGetBranchConfigValue(
1375 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001376 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 return self.patchset
1378
1379 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001380 """Set this branch's patchset. If patchset=0, clears the patchset."""
1381 assert self.GetBranch()
1382 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001383 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001384 else:
1385 self.patchset = int(patchset)
1386 self._GitSetBranchConfigValue(
1387 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001389 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001390 """Set this branch's issue. If issue isn't given, clears the issue."""
1391 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001393 issue = int(issue)
1394 self._GitSetBranchConfigValue(
1395 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001396 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001397 codereview_server = self._codereview_impl.GetCodereviewServer()
1398 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001399 self._GitSetBranchConfigValue(
1400 self._codereview_impl.CodereviewServerConfigKey(),
1401 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 else:
tandrii5d48c322016-08-18 16:19:37 -07001403 # Reset all of these just to be clean.
1404 reset_suffixes = [
1405 'last-upload-hash',
1406 self._codereview_impl.IssueConfigKey(),
1407 self._codereview_impl.PatchsetConfigKey(),
1408 self._codereview_impl.CodereviewServerConfigKey(),
1409 ] + self._PostUnsetIssueProperties()
1410 for prop in reset_suffixes:
1411 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001412 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001413 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414
dnjba1b0f32016-09-02 12:37:42 -07001415 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001416 if not self.GitSanityChecks(upstream_branch):
1417 DieWithError('\nGit sanity check failure')
1418
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001419 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001420 if not root:
1421 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001422 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001423
1424 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001425 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001426 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001427 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001428 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001429 except subprocess2.CalledProcessError:
1430 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001431 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001432 'This branch probably doesn\'t exist anymore. To reset the\n'
1433 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001434 ' git branch --set-upstream-to origin/master %s\n'
1435 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001436 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001437
maruel@chromium.org52424302012-08-29 15:14:30 +00001438 issue = self.GetIssue()
1439 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001440 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001441 description = self.GetDescription()
1442 else:
1443 # If the change was never uploaded, use the log messages of all commits
1444 # up to the branch point, as git cl upload will prefill the description
1445 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001446 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1447 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001448
1449 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001450 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001451 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001452 name,
1453 description,
1454 absroot,
1455 files,
1456 issue,
1457 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001458 author,
1459 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001460
dsansomee2d6fd92016-09-08 00:10:47 -07001461 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001462 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001463 return self._codereview_impl.UpdateDescriptionRemote(
1464 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001465
1466 def RunHook(self, committing, may_prompt, verbose, change):
1467 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1468 try:
1469 return presubmit_support.DoPresubmitChecks(change, committing,
1470 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1471 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001472 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1473 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001474 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001475 DieWithError(
1476 ('%s\nMaybe your depot_tools is out of date?\n'
1477 'If all fails, contact maruel@') % e)
1478
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001479 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1480 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001481 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1482 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001483 else:
1484 # Assume url.
1485 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1486 urlparse.urlparse(issue_arg))
1487 if not parsed_issue_arg or not parsed_issue_arg.valid:
1488 DieWithError('Failed to parse issue argument "%s". '
1489 'Must be an issue number or a valid URL.' % issue_arg)
1490 return self._codereview_impl.CMDPatchWithParsedIssue(
1491 parsed_issue_arg, reject, nocommit, directory)
1492
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001493 def CMDUpload(self, options, git_diff_args, orig_args):
1494 """Uploads a change to codereview."""
1495 if git_diff_args:
1496 # TODO(ukai): is it ok for gerrit case?
1497 base_branch = git_diff_args[0]
1498 else:
1499 if self.GetBranch() is None:
1500 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1501
1502 # Default to diffing against common ancestor of upstream branch
1503 base_branch = self.GetCommonAncestorWithUpstream()
1504 git_diff_args = [base_branch, 'HEAD']
1505
1506 # Make sure authenticated to codereview before running potentially expensive
1507 # hooks. It is a fast, best efforts check. Codereview still can reject the
1508 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001509 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001510
1511 # Apply watchlists on upload.
1512 change = self.GetChange(base_branch, None)
1513 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1514 files = [f.LocalPath() for f in change.AffectedFiles()]
1515 if not options.bypass_watchlists:
1516 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1517
1518 if not options.bypass_hooks:
1519 if options.reviewers or options.tbr_owners:
1520 # Set the reviewer list now so that presubmit checks can access it.
1521 change_description = ChangeDescription(change.FullDescriptionText())
1522 change_description.update_reviewers(options.reviewers,
1523 options.tbr_owners,
1524 change)
1525 change.SetDescriptionText(change_description.description)
1526 hook_results = self.RunHook(committing=False,
1527 may_prompt=not options.force,
1528 verbose=options.verbose,
1529 change=change)
1530 if not hook_results.should_continue():
1531 return 1
1532 if not options.reviewers and hook_results.reviewers:
1533 options.reviewers = hook_results.reviewers.split(',')
1534
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001535 # TODO(tandrii): Checking local patchset against remote patchset is only
1536 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1537 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001538 latest_patchset = self.GetMostRecentPatchset()
1539 local_patchset = self.GetPatchset()
1540 if (latest_patchset and local_patchset and
1541 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001542 print('The last upload made from this repository was patchset #%d but '
1543 'the most recent patchset on the server is #%d.'
1544 % (local_patchset, latest_patchset))
1545 print('Uploading will still work, but if you\'ve uploaded to this '
1546 'issue from another machine or branch the patch you\'re '
1547 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 ask_for_data('About to upload; enter to confirm.')
1549
1550 print_stats(options.similarity, options.find_copies, git_diff_args)
1551 ret = self.CMDUploadChange(options, git_diff_args, change)
1552 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001553 if options.use_commit_queue:
1554 self.SetCQState(_CQState.COMMIT)
1555 elif options.cq_dry_run:
1556 self.SetCQState(_CQState.DRY_RUN)
1557
tandrii5d48c322016-08-18 16:19:37 -07001558 _git_set_branch_config_value('last-upload-hash',
1559 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001560 # Run post upload hooks, if specified.
1561 if settings.GetRunPostUploadHook():
1562 presubmit_support.DoPostUploadExecuter(
1563 change,
1564 self,
1565 settings.GetRoot(),
1566 options.verbose,
1567 sys.stdout)
1568
1569 # Upload all dependencies if specified.
1570 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001571 print()
1572 print('--dependencies has been specified.')
1573 print('All dependent local branches will be re-uploaded.')
1574 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001575 # Remove the dependencies flag from args so that we do not end up in a
1576 # loop.
1577 orig_args.remove('--dependencies')
1578 ret = upload_branch_deps(self, orig_args)
1579 return ret
1580
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001581 def SetCQState(self, new_state):
1582 """Update the CQ state for latest patchset.
1583
1584 Issue must have been already uploaded and known.
1585 """
1586 assert new_state in _CQState.ALL_STATES
1587 assert self.GetIssue()
1588 return self._codereview_impl.SetCQState(new_state)
1589
qyearsley1fdfcb62016-10-24 13:22:03 -07001590 def TriggerDryRun(self):
1591 """Triggers a dry run and prints a warning on failure."""
1592 # TODO(qyearsley): Either re-use this method in CMDset_commit
1593 # and CMDupload, or change CMDtry to trigger dry runs with
1594 # just SetCQState, and catch keyboard interrupt and other
1595 # errors in that method.
1596 try:
1597 self.SetCQState(_CQState.DRY_RUN)
1598 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1599 return 0
1600 except KeyboardInterrupt:
1601 raise
1602 except:
1603 print('WARNING: failed to trigger CQ Dry Run.\n'
1604 'Either:\n'
1605 ' * your project has no CQ\n'
1606 ' * you don\'t have permission to trigger Dry Run\n'
1607 ' * bug in this code (see stack trace below).\n'
1608 'Consider specifying which bots to trigger manually '
1609 'or asking your project owners for permissions '
1610 'or contacting Chrome Infrastructure team at '
1611 'https://www.chromium.org/infra\n\n')
1612 # Still raise exception so that stack trace is printed.
1613 raise
1614
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001615 # Forward methods to codereview specific implementation.
1616
1617 def CloseIssue(self):
1618 return self._codereview_impl.CloseIssue()
1619
1620 def GetStatus(self):
1621 return self._codereview_impl.GetStatus()
1622
1623 def GetCodereviewServer(self):
1624 return self._codereview_impl.GetCodereviewServer()
1625
tandriide281ae2016-10-12 06:02:30 -07001626 def GetIssueOwner(self):
1627 """Get owner from codereview, which may differ from this checkout."""
1628 return self._codereview_impl.GetIssueOwner()
1629
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001630 def GetApprovingReviewers(self):
1631 return self._codereview_impl.GetApprovingReviewers()
1632
1633 def GetMostRecentPatchset(self):
1634 return self._codereview_impl.GetMostRecentPatchset()
1635
tandriide281ae2016-10-12 06:02:30 -07001636 def CannotTriggerTryJobReason(self):
1637 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1638 return self._codereview_impl.CannotTriggerTryJobReason()
1639
tandrii8c5a3532016-11-04 07:52:02 -07001640 def GetTryjobProperties(self, patchset=None):
1641 """Returns dictionary of properties to launch tryjob."""
1642 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1643
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001644 def __getattr__(self, attr):
1645 # This is because lots of untested code accesses Rietveld-specific stuff
1646 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001647 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001648 # Note that child method defines __getattr__ as well, and forwards it here,
1649 # because _RietveldChangelistImpl is not cleaned up yet, and given
1650 # deprecation of Rietveld, it should probably be just removed.
1651 # Until that time, avoid infinite recursion by bypassing __getattr__
1652 # of implementation class.
1653 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001654
1655
1656class _ChangelistCodereviewBase(object):
1657 """Abstract base class encapsulating codereview specifics of a changelist."""
1658 def __init__(self, changelist):
1659 self._changelist = changelist # instance of Changelist
1660
1661 def __getattr__(self, attr):
1662 # Forward methods to changelist.
1663 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1664 # _RietveldChangelistImpl to avoid this hack?
1665 return getattr(self._changelist, attr)
1666
1667 def GetStatus(self):
1668 """Apply a rough heuristic to give a simple summary of an issue's review
1669 or CQ status, assuming adherence to a common workflow.
1670
1671 Returns None if no issue for this branch, or specific string keywords.
1672 """
1673 raise NotImplementedError()
1674
1675 def GetCodereviewServer(self):
1676 """Returns server URL without end slash, like "https://codereview.com"."""
1677 raise NotImplementedError()
1678
1679 def FetchDescription(self):
1680 """Fetches and returns description from the codereview server."""
1681 raise NotImplementedError()
1682
tandrii5d48c322016-08-18 16:19:37 -07001683 @classmethod
1684 def IssueConfigKey(cls):
1685 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001686 raise NotImplementedError()
1687
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001688 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001689 def PatchsetConfigKey(cls):
1690 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001691 raise NotImplementedError()
1692
tandrii5d48c322016-08-18 16:19:37 -07001693 @classmethod
1694 def CodereviewServerConfigKey(cls):
1695 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001696 raise NotImplementedError()
1697
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001698 def _PostUnsetIssueProperties(self):
1699 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001700 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001701
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001702 def GetRieveldObjForPresubmit(self):
1703 # This is an unfortunate Rietveld-embeddedness in presubmit.
1704 # For non-Rietveld codereviews, this probably should return a dummy object.
1705 raise NotImplementedError()
1706
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001707 def GetGerritObjForPresubmit(self):
1708 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1709 return None
1710
dsansomee2d6fd92016-09-08 00:10:47 -07001711 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001712 """Update the description on codereview site."""
1713 raise NotImplementedError()
1714
1715 def CloseIssue(self):
1716 """Closes the issue."""
1717 raise NotImplementedError()
1718
1719 def GetApprovingReviewers(self):
1720 """Returns a list of reviewers approving the change.
1721
1722 Note: not necessarily committers.
1723 """
1724 raise NotImplementedError()
1725
1726 def GetMostRecentPatchset(self):
1727 """Returns the most recent patchset number from the codereview site."""
1728 raise NotImplementedError()
1729
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001730 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1731 directory):
1732 """Fetches and applies the issue.
1733
1734 Arguments:
1735 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1736 reject: if True, reject the failed patch instead of switching to 3-way
1737 merge. Rietveld only.
1738 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1739 only.
1740 directory: switch to directory before applying the patch. Rietveld only.
1741 """
1742 raise NotImplementedError()
1743
1744 @staticmethod
1745 def ParseIssueURL(parsed_url):
1746 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1747 failed."""
1748 raise NotImplementedError()
1749
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001750 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001751 """Best effort check that user is authenticated with codereview server.
1752
1753 Arguments:
1754 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001755 refresh: whether to attempt to refresh credentials. Ignored if not
1756 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001757 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001758 raise NotImplementedError()
1759
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001760 def CMDUploadChange(self, options, args, change):
1761 """Uploads a change to codereview."""
1762 raise NotImplementedError()
1763
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001764 def SetCQState(self, new_state):
1765 """Update the CQ state for latest patchset.
1766
1767 Issue must have been already uploaded and known.
1768 """
1769 raise NotImplementedError()
1770
tandriie113dfd2016-10-11 10:20:12 -07001771 def CannotTriggerTryJobReason(self):
1772 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1773 raise NotImplementedError()
1774
tandriide281ae2016-10-12 06:02:30 -07001775 def GetIssueOwner(self):
1776 raise NotImplementedError()
1777
tandrii8c5a3532016-11-04 07:52:02 -07001778 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001779 raise NotImplementedError()
1780
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001781
1782class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001783 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001784 super(_RietveldChangelistImpl, self).__init__(changelist)
1785 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001786 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001787 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001788
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001789 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001790 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001791 self._props = None
1792 self._rpc_server = None
1793
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001794 def GetCodereviewServer(self):
1795 if not self._rietveld_server:
1796 # If we're on a branch then get the server potentially associated
1797 # with that branch.
1798 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001799 self._rietveld_server = gclient_utils.UpgradeToHttps(
1800 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001801 if not self._rietveld_server:
1802 self._rietveld_server = settings.GetDefaultServerUrl()
1803 return self._rietveld_server
1804
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001805 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001806 """Best effort check that user is authenticated with Rietveld server."""
1807 if self._auth_config.use_oauth2:
1808 authenticator = auth.get_authenticator_for_host(
1809 self.GetCodereviewServer(), self._auth_config)
1810 if not authenticator.has_cached_credentials():
1811 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001812 if refresh:
1813 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001814
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001815 def FetchDescription(self):
1816 issue = self.GetIssue()
1817 assert issue
1818 try:
1819 return self.RpcServer().get_description(issue).strip()
1820 except urllib2.HTTPError as e:
1821 if e.code == 404:
1822 DieWithError(
1823 ('\nWhile fetching the description for issue %d, received a '
1824 '404 (not found)\n'
1825 'error. It is likely that you deleted this '
1826 'issue on the server. If this is the\n'
1827 'case, please run\n\n'
1828 ' git cl issue 0\n\n'
1829 'to clear the association with the deleted issue. Then run '
1830 'this command again.') % issue)
1831 else:
1832 DieWithError(
1833 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1834 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001835 print('Warning: Failed to retrieve CL description due to network '
1836 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001837 return ''
1838
1839 def GetMostRecentPatchset(self):
1840 return self.GetIssueProperties()['patchsets'][-1]
1841
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842 def GetIssueProperties(self):
1843 if self._props is None:
1844 issue = self.GetIssue()
1845 if not issue:
1846 self._props = {}
1847 else:
1848 self._props = self.RpcServer().get_issue_properties(issue, True)
1849 return self._props
1850
tandriie113dfd2016-10-11 10:20:12 -07001851 def CannotTriggerTryJobReason(self):
1852 props = self.GetIssueProperties()
1853 if not props:
1854 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1855 if props.get('closed'):
1856 return 'CL %s is closed' % self.GetIssue()
1857 if props.get('private'):
1858 return 'CL %s is private' % self.GetIssue()
1859 return None
1860
tandrii8c5a3532016-11-04 07:52:02 -07001861 def GetTryjobProperties(self, patchset=None):
1862 """Returns dictionary of properties to launch tryjob."""
1863 project = (self.GetIssueProperties() or {}).get('project')
1864 return {
1865 'issue': self.GetIssue(),
1866 'patch_project': project,
1867 'patch_storage': 'rietveld',
1868 'patchset': patchset or self.GetPatchset(),
1869 'rietveld': self.GetCodereviewServer(),
1870 }
1871
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001872 def GetApprovingReviewers(self):
1873 return get_approving_reviewers(self.GetIssueProperties())
1874
tandriide281ae2016-10-12 06:02:30 -07001875 def GetIssueOwner(self):
1876 return (self.GetIssueProperties() or {}).get('owner_email')
1877
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001878 def AddComment(self, message):
1879 return self.RpcServer().add_comment(self.GetIssue(), message)
1880
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001881 def GetStatus(self):
1882 """Apply a rough heuristic to give a simple summary of an issue's review
1883 or CQ status, assuming adherence to a common workflow.
1884
1885 Returns None if no issue for this branch, or one of the following keywords:
1886 * 'error' - error from review tool (including deleted issues)
1887 * 'unsent' - not sent for review
1888 * 'waiting' - waiting for review
1889 * 'reply' - waiting for owner to reply to review
1890 * 'lgtm' - LGTM from at least one approved reviewer
1891 * 'commit' - in the commit queue
1892 * 'closed' - closed
1893 """
1894 if not self.GetIssue():
1895 return None
1896
1897 try:
1898 props = self.GetIssueProperties()
1899 except urllib2.HTTPError:
1900 return 'error'
1901
1902 if props.get('closed'):
1903 # Issue is closed.
1904 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001905 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001906 # Issue is in the commit queue.
1907 return 'commit'
1908
1909 try:
1910 reviewers = self.GetApprovingReviewers()
1911 except urllib2.HTTPError:
1912 return 'error'
1913
1914 if reviewers:
1915 # Was LGTM'ed.
1916 return 'lgtm'
1917
1918 messages = props.get('messages') or []
1919
tandrii9d2c7a32016-06-22 03:42:45 -07001920 # Skip CQ messages that don't require owner's action.
1921 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1922 if 'Dry run:' in messages[-1]['text']:
1923 messages.pop()
1924 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1925 # This message always follows prior messages from CQ,
1926 # so skip this too.
1927 messages.pop()
1928 else:
1929 # This is probably a CQ messages warranting user attention.
1930 break
1931
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001932 if not messages:
1933 # No message was sent.
1934 return 'unsent'
1935 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001936 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001937 return 'reply'
1938 return 'waiting'
1939
dsansomee2d6fd92016-09-08 00:10:47 -07001940 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001941 return self.RpcServer().update_description(
1942 self.GetIssue(), self.description)
1943
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001944 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001945 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001946
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001947 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001948 return self.SetFlags({flag: value})
1949
1950 def SetFlags(self, flags):
1951 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001952 """
phajdan.jr68598232016-08-10 03:28:28 -07001953 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001954 try:
tandrii4b233bd2016-07-06 03:50:29 -07001955 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001956 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001957 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001958 if e.code == 404:
1959 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1960 if e.code == 403:
1961 DieWithError(
1962 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001963 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001964 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001966 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001967 """Returns an upload.RpcServer() to access this review's rietveld instance.
1968 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001969 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001970 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001971 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001972 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001973 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001975 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001976 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001977 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001978
tandrii5d48c322016-08-18 16:19:37 -07001979 @classmethod
1980 def PatchsetConfigKey(cls):
1981 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001982
tandrii5d48c322016-08-18 16:19:37 -07001983 @classmethod
1984 def CodereviewServerConfigKey(cls):
1985 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001986
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001987 def GetRieveldObjForPresubmit(self):
1988 return self.RpcServer()
1989
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001990 def SetCQState(self, new_state):
1991 props = self.GetIssueProperties()
1992 if props.get('private'):
1993 DieWithError('Cannot set-commit on private issue')
1994
1995 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001996 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001997 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001998 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001999 else:
tandrii4b233bd2016-07-06 03:50:29 -07002000 assert new_state == _CQState.DRY_RUN
2001 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002002
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002003 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2004 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002005 # PatchIssue should never be called with a dirty tree. It is up to the
2006 # caller to check this, but just in case we assert here since the
2007 # consequences of the caller not checking this could be dire.
2008 assert(not git_common.is_dirty_git_tree('apply'))
2009 assert(parsed_issue_arg.valid)
2010 self._changelist.issue = parsed_issue_arg.issue
2011 if parsed_issue_arg.hostname:
2012 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2013
skobes6468b902016-10-24 08:45:10 -07002014 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2015 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2016 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002017 try:
skobes6468b902016-10-24 08:45:10 -07002018 scm_obj.apply_patch(patchset_object)
2019 except Exception as e:
2020 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002021 return 1
2022
2023 # If we had an issue, commit the current state and register the issue.
2024 if not nocommit:
2025 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2026 'patch from issue %(i)s at patchset '
2027 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2028 % {'i': self.GetIssue(), 'p': patchset})])
2029 self.SetIssue(self.GetIssue())
2030 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002031 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002032 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002033 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002034 return 0
2035
2036 @staticmethod
2037 def ParseIssueURL(parsed_url):
2038 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2039 return None
wychen3c1c1722016-08-04 11:46:36 -07002040 # Rietveld patch: https://domain/<number>/#ps<patchset>
2041 match = re.match(r'/(\d+)/$', parsed_url.path)
2042 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2043 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002044 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002045 issue=int(match.group(1)),
2046 patchset=int(match2.group(1)),
2047 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002048 # Typical url: https://domain/<issue_number>[/[other]]
2049 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2050 if match:
skobes6468b902016-10-24 08:45:10 -07002051 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002052 issue=int(match.group(1)),
2053 hostname=parsed_url.netloc)
2054 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2055 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2056 if match:
skobes6468b902016-10-24 08:45:10 -07002057 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002058 issue=int(match.group(1)),
2059 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002060 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002061 return None
2062
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002063 def CMDUploadChange(self, options, args, change):
2064 """Upload the patch to Rietveld."""
2065 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2066 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002067 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2068 if options.emulate_svn_auto_props:
2069 upload_args.append('--emulate_svn_auto_props')
2070
2071 change_desc = None
2072
2073 if options.email is not None:
2074 upload_args.extend(['--email', options.email])
2075
2076 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002077 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002078 upload_args.extend(['--title', options.title])
2079 if options.message:
2080 upload_args.extend(['--message', options.message])
2081 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002082 print('This branch is associated with issue %s. '
2083 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002084 else:
nodirca166002016-06-27 10:59:51 -07002085 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002086 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002087 if options.message:
2088 message = options.message
2089 else:
2090 message = CreateDescriptionFromLog(args)
2091 if options.title:
2092 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002093 change_desc = ChangeDescription(message)
2094 if options.reviewers or options.tbr_owners:
2095 change_desc.update_reviewers(options.reviewers,
2096 options.tbr_owners,
2097 change)
2098 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002099 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002100
2101 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002102 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002103 return 1
2104
2105 upload_args.extend(['--message', change_desc.description])
2106 if change_desc.get_reviewers():
2107 upload_args.append('--reviewers=%s' % ','.join(
2108 change_desc.get_reviewers()))
2109 if options.send_mail:
2110 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002111 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002112 upload_args.append('--send_mail')
2113
2114 # We check this before applying rietveld.private assuming that in
2115 # rietveld.cc only addresses which we can send private CLs to are listed
2116 # if rietveld.private is set, and so we should ignore rietveld.cc only
2117 # when --private is specified explicitly on the command line.
2118 if options.private:
2119 logging.warn('rietveld.cc is ignored since private flag is specified. '
2120 'You need to review and add them manually if necessary.')
2121 cc = self.GetCCListWithoutDefault()
2122 else:
2123 cc = self.GetCCList()
2124 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002125 if change_desc.get_cced():
2126 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002127 if cc:
2128 upload_args.extend(['--cc', cc])
2129
2130 if options.private or settings.GetDefaultPrivateFlag() == "True":
2131 upload_args.append('--private')
2132
2133 upload_args.extend(['--git_similarity', str(options.similarity)])
2134 if not options.find_copies:
2135 upload_args.extend(['--git_no_find_copies'])
2136
2137 # Include the upstream repo's URL in the change -- this is useful for
2138 # projects that have their source spread across multiple repos.
2139 remote_url = self.GetGitBaseUrlFromConfig()
2140 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002141 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2142 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2143 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002144 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002145 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002146 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002147 if target_ref:
2148 upload_args.extend(['--target_ref', target_ref])
2149
2150 # Look for dependent patchsets. See crbug.com/480453 for more details.
2151 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2152 upstream_branch = ShortBranchName(upstream_branch)
2153 if remote is '.':
2154 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002155 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002156 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002157 print()
2158 print('Skipping dependency patchset upload because git config '
2159 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2160 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002161 else:
2162 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002163 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002164 auth_config=auth_config)
2165 branch_cl_issue_url = branch_cl.GetIssueURL()
2166 branch_cl_issue = branch_cl.GetIssue()
2167 branch_cl_patchset = branch_cl.GetPatchset()
2168 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2169 upload_args.extend(
2170 ['--depends_on_patchset', '%s:%s' % (
2171 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002172 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002173 '\n'
2174 'The current branch (%s) is tracking a local branch (%s) with '
2175 'an associated CL.\n'
2176 'Adding %s/#ps%s as a dependency patchset.\n'
2177 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2178 branch_cl_patchset))
2179
2180 project = settings.GetProject()
2181 if project:
2182 upload_args.extend(['--project', project])
2183
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002184 try:
2185 upload_args = ['upload'] + upload_args + args
2186 logging.info('upload.RealMain(%s)', upload_args)
2187 issue, patchset = upload.RealMain(upload_args)
2188 issue = int(issue)
2189 patchset = int(patchset)
2190 except KeyboardInterrupt:
2191 sys.exit(1)
2192 except:
2193 # If we got an exception after the user typed a description for their
2194 # change, back up the description before re-raising.
2195 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002196 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002197 raise
2198
2199 if not self.GetIssue():
2200 self.SetIssue(issue)
2201 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002202 return 0
2203
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002204
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002205class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002206 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002207 # auth_config is Rietveld thing, kept here to preserve interface only.
2208 super(_GerritChangelistImpl, self).__init__(changelist)
2209 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002210 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002211 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002212 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002213 # Map from change number (issue) to its detail cache.
2214 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002215
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002216 if codereview_host is not None:
2217 assert not codereview_host.startswith('https://'), codereview_host
2218 self._gerrit_host = codereview_host
2219 self._gerrit_server = 'https://%s' % codereview_host
2220
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002221 def _GetGerritHost(self):
2222 # Lazy load of configs.
2223 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002224 if self._gerrit_host and '.' not in self._gerrit_host:
2225 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2226 # This happens for internal stuff http://crbug.com/614312.
2227 parsed = urlparse.urlparse(self.GetRemoteUrl())
2228 if parsed.scheme == 'sso':
2229 print('WARNING: using non https URLs for remote is likely broken\n'
2230 ' Your current remote is: %s' % self.GetRemoteUrl())
2231 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2232 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002233 return self._gerrit_host
2234
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002235 def _GetGitHost(self):
2236 """Returns git host to be used when uploading change to Gerrit."""
2237 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2238
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002239 def GetCodereviewServer(self):
2240 if not self._gerrit_server:
2241 # If we're on a branch then get the server potentially associated
2242 # with that branch.
2243 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002244 self._gerrit_server = self._GitGetBranchConfigValue(
2245 self.CodereviewServerConfigKey())
2246 if self._gerrit_server:
2247 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002248 if not self._gerrit_server:
2249 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2250 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002251 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002252 parts[0] = parts[0] + '-review'
2253 self._gerrit_host = '.'.join(parts)
2254 self._gerrit_server = 'https://%s' % self._gerrit_host
2255 return self._gerrit_server
2256
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002257 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002258 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002259 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002260
tandrii5d48c322016-08-18 16:19:37 -07002261 @classmethod
2262 def PatchsetConfigKey(cls):
2263 return 'gerritpatchset'
2264
2265 @classmethod
2266 def CodereviewServerConfigKey(cls):
2267 return 'gerritserver'
2268
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002269 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002270 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002271 if settings.GetGerritSkipEnsureAuthenticated():
2272 # For projects with unusual authentication schemes.
2273 # See http://crbug.com/603378.
2274 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002275 # Lazy-loader to identify Gerrit and Git hosts.
2276 if gerrit_util.GceAuthenticator.is_gce():
2277 return
2278 self.GetCodereviewServer()
2279 git_host = self._GetGitHost()
2280 assert self._gerrit_server and self._gerrit_host
2281 cookie_auth = gerrit_util.CookiesAuthenticator()
2282
2283 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2284 git_auth = cookie_auth.get_auth_header(git_host)
2285 if gerrit_auth and git_auth:
2286 if gerrit_auth == git_auth:
2287 return
2288 print((
2289 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2290 ' Check your %s or %s file for credentials of hosts:\n'
2291 ' %s\n'
2292 ' %s\n'
2293 ' %s') %
2294 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2295 git_host, self._gerrit_host,
2296 cookie_auth.get_new_password_message(git_host)))
2297 if not force:
2298 ask_for_data('If you know what you are doing, press Enter to continue, '
2299 'Ctrl+C to abort.')
2300 return
2301 else:
2302 missing = (
2303 [] if gerrit_auth else [self._gerrit_host] +
2304 [] if git_auth else [git_host])
2305 DieWithError('Credentials for the following hosts are required:\n'
2306 ' %s\n'
2307 'These are read from %s (or legacy %s)\n'
2308 '%s' % (
2309 '\n '.join(missing),
2310 cookie_auth.get_gitcookies_path(),
2311 cookie_auth.get_netrc_path(),
2312 cookie_auth.get_new_password_message(git_host)))
2313
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002314 def _PostUnsetIssueProperties(self):
2315 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002316 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002317
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002318 def GetRieveldObjForPresubmit(self):
2319 class ThisIsNotRietveldIssue(object):
2320 def __nonzero__(self):
2321 # This is a hack to make presubmit_support think that rietveld is not
2322 # defined, yet still ensure that calls directly result in a decent
2323 # exception message below.
2324 return False
2325
2326 def __getattr__(self, attr):
2327 print(
2328 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2329 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2330 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2331 'or use Rietveld for codereview.\n'
2332 'See also http://crbug.com/579160.' % attr)
2333 raise NotImplementedError()
2334 return ThisIsNotRietveldIssue()
2335
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002336 def GetGerritObjForPresubmit(self):
2337 return presubmit_support.GerritAccessor(self._GetGerritHost())
2338
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002339 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002340 """Apply a rough heuristic to give a simple summary of an issue's review
2341 or CQ status, assuming adherence to a common workflow.
2342
2343 Returns None if no issue for this branch, or one of the following keywords:
2344 * 'error' - error from review tool (including deleted issues)
2345 * 'unsent' - no reviewers added
2346 * 'waiting' - waiting for review
2347 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002348 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002349 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002350 * 'commit' - in the commit queue
2351 * 'closed' - abandoned
2352 """
2353 if not self.GetIssue():
2354 return None
2355
2356 try:
2357 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002358 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002359 return 'error'
2360
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002361 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002362 return 'closed'
2363
2364 cq_label = data['labels'].get('Commit-Queue', {})
2365 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002366 votes = cq_label.get('all', [])
2367 highest_vote = 0
2368 for v in votes:
2369 highest_vote = max(highest_vote, v.get('value', 0))
2370 vote_value = str(highest_vote)
2371 if vote_value != '0':
2372 # Add a '+' if the value is not 0 to match the values in the label.
2373 # The cq_label does not have negatives.
2374 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002375 vote_text = cq_label.get('values', {}).get(vote_value, '')
2376 if vote_text.lower() == 'commit':
2377 return 'commit'
2378
2379 lgtm_label = data['labels'].get('Code-Review', {})
2380 if lgtm_label:
2381 if 'rejected' in lgtm_label:
2382 return 'not lgtm'
2383 if 'approved' in lgtm_label:
2384 return 'lgtm'
2385
2386 if not data.get('reviewers', {}).get('REVIEWER', []):
2387 return 'unsent'
2388
2389 messages = data.get('messages', [])
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002390 owner = data['owner'].get('_account_id')
2391 while messages:
2392 last_message_author = messages.pop().get('author', {})
2393 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2394 # Ignore replies from CQ.
2395 continue
2396 if owner != last_message_author.get('_account_id'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002397 # Some reply from non-owner.
2398 return 'reply'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002399 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002400
2401 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002402 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002403 return data['revisions'][data['current_revision']]['_number']
2404
2405 def FetchDescription(self):
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002406 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002407 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002408 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002409
dsansomee2d6fd92016-09-08 00:10:47 -07002410 def UpdateDescriptionRemote(self, description, force=False):
2411 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2412 if not force:
2413 ask_for_data(
2414 'The description cannot be modified while the issue has a pending '
2415 'unpublished edit. Either publish the edit in the Gerrit web UI '
2416 'or delete it.\n\n'
2417 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2418
2419 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2420 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002421 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002422 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002423
2424 def CloseIssue(self):
2425 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2426
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002427 def GetApprovingReviewers(self):
2428 """Returns a list of reviewers approving the change.
2429
2430 Note: not necessarily committers.
2431 """
2432 raise NotImplementedError()
2433
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002434 def SubmitIssue(self, wait_for_merge=True):
2435 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2436 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002437
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002438 def _GetChangeDetail(self, options=None, issue=None,
2439 no_cache=False):
2440 """Returns details of the issue by querying Gerrit and caching results.
2441
2442 If fresh data is needed, set no_cache=True which will clear cache and
2443 thus new data will be fetched from Gerrit.
2444 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002445 options = options or []
2446 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002447 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002448
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002449 # Optimization to avoid multiple RPCs:
2450 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2451 'CURRENT_COMMIT' not in options):
2452 options.append('CURRENT_COMMIT')
2453
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002454 # Normalize issue and options for consistent keys in cache.
2455 issue = str(issue)
2456 options = [o.upper() for o in options]
2457
2458 # Check in cache first unless no_cache is True.
2459 if no_cache:
2460 self._detail_cache.pop(issue, None)
2461 else:
2462 options_set = frozenset(options)
2463 for cached_options_set, data in self._detail_cache.get(issue, []):
2464 # Assumption: data fetched before with extra options is suitable
2465 # for return for a smaller set of options.
2466 # For example, if we cached data for
2467 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2468 # and request is for options=[CURRENT_REVISION],
2469 # THEN we can return prior cached data.
2470 if options_set.issubset(cached_options_set):
2471 return data
2472
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002473 try:
2474 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2475 options, ignore_404=False)
2476 except gerrit_util.GerritError as e:
2477 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002478 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002479 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002480
2481 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002482 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002483
agable32978d92016-11-01 12:55:02 -07002484 def _GetChangeCommit(self, issue=None):
2485 issue = issue or self.GetIssue()
2486 assert issue, 'issue is required to query Gerrit'
2487 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2488 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002489 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002490 return data
2491
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002492 def CMDLand(self, force, bypass_hooks, verbose):
2493 if git_common.is_dirty_git_tree('land'):
2494 return 1
tandriid60367b2016-06-22 05:25:12 -07002495 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2496 if u'Commit-Queue' in detail.get('labels', {}):
2497 if not force:
2498 ask_for_data('\nIt seems this repository has a Commit Queue, '
2499 'which can test and land changes for you. '
2500 'Are you sure you wish to bypass it?\n'
2501 'Press Enter to continue, Ctrl+C to abort.')
2502
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002503 differs = True
tandriic4344b52016-08-29 06:04:54 -07002504 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002505 # Note: git diff outputs nothing if there is no diff.
2506 if not last_upload or RunGit(['diff', last_upload]).strip():
2507 print('WARNING: some changes from local branch haven\'t been uploaded')
2508 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002509 if detail['current_revision'] == last_upload:
2510 differs = False
2511 else:
2512 print('WARNING: local branch contents differ from latest uploaded '
2513 'patchset')
2514 if differs:
2515 if not force:
2516 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002517 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2518 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002519 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2520 elif not bypass_hooks:
2521 hook_results = self.RunHook(
2522 committing=True,
2523 may_prompt=not force,
2524 verbose=verbose,
2525 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2526 if not hook_results.should_continue():
2527 return 1
2528
2529 self.SubmitIssue(wait_for_merge=True)
2530 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002531 links = self._GetChangeCommit().get('web_links', [])
2532 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002533 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002534 print('Landed as %s' % link.get('url'))
2535 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002536 return 0
2537
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002538 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2539 directory):
2540 assert not reject
2541 assert not nocommit
2542 assert not directory
2543 assert parsed_issue_arg.valid
2544
2545 self._changelist.issue = parsed_issue_arg.issue
2546
2547 if parsed_issue_arg.hostname:
2548 self._gerrit_host = parsed_issue_arg.hostname
2549 self._gerrit_server = 'https://%s' % self._gerrit_host
2550
tandriic2405f52016-10-10 08:13:15 -07002551 try:
2552 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002553 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002554 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002555
2556 if not parsed_issue_arg.patchset:
2557 # Use current revision by default.
2558 revision_info = detail['revisions'][detail['current_revision']]
2559 patchset = int(revision_info['_number'])
2560 else:
2561 patchset = parsed_issue_arg.patchset
2562 for revision_info in detail['revisions'].itervalues():
2563 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2564 break
2565 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002566 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002567 (parsed_issue_arg.patchset, self.GetIssue()))
2568
2569 fetch_info = revision_info['fetch']['http']
2570 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2571 RunGit(['cherry-pick', 'FETCH_HEAD'])
2572 self.SetIssue(self.GetIssue())
2573 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002574 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002575 (self.GetIssue(), self.GetPatchset()))
2576 return 0
2577
2578 @staticmethod
2579 def ParseIssueURL(parsed_url):
2580 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2581 return None
2582 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2583 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2584 # Short urls like https://domain/<issue_number> can be used, but don't allow
2585 # specifying the patchset (you'd 404), but we allow that here.
2586 if parsed_url.path == '/':
2587 part = parsed_url.fragment
2588 else:
2589 part = parsed_url.path
2590 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2591 if match:
2592 return _ParsedIssueNumberArgument(
2593 issue=int(match.group(2)),
2594 patchset=int(match.group(4)) if match.group(4) else None,
2595 hostname=parsed_url.netloc)
2596 return None
2597
tandrii16e0b4e2016-06-07 10:34:28 -07002598 def _GerritCommitMsgHookCheck(self, offer_removal):
2599 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2600 if not os.path.exists(hook):
2601 return
2602 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2603 # custom developer made one.
2604 data = gclient_utils.FileRead(hook)
2605 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2606 return
2607 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002608 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002609 'and may interfere with it in subtle ways.\n'
2610 'We recommend you remove the commit-msg hook.')
2611 if offer_removal:
2612 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2613 if reply.lower().startswith('y'):
2614 gclient_utils.rm_file_or_tree(hook)
2615 print('Gerrit commit-msg hook removed.')
2616 else:
2617 print('OK, will keep Gerrit commit-msg hook in place.')
2618
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002619 def CMDUploadChange(self, options, args, change):
2620 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002621 if options.squash and options.no_squash:
2622 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002623
2624 if not options.squash and not options.no_squash:
2625 # Load default for user, repo, squash=true, in this order.
2626 options.squash = settings.GetSquashGerritUploads()
2627 elif options.no_squash:
2628 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002629
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002630 # We assume the remote called "origin" is the one we want.
2631 # It is probably not worthwhile to support different workflows.
2632 gerrit_remote = 'origin'
2633
2634 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002635 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002636
Aaron Gableb56ad332017-01-06 15:24:31 -08002637 # This may be None; default fallback value is determined in logic below.
2638 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002639 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002640
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002641 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002642 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002643 if self.GetIssue():
2644 # Try to get the message from a previous upload.
2645 message = self.GetDescription()
2646 if not message:
2647 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002648 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002649 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002650 if not title:
2651 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2652 title = ask_for_data(
2653 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002654 if title == default_title:
2655 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002656 change_id = self._GetChangeDetail()['change_id']
2657 while True:
2658 footer_change_ids = git_footers.get_footer_change_id(message)
2659 if footer_change_ids == [change_id]:
2660 break
2661 if not footer_change_ids:
2662 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002663 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002664 continue
2665 # There is already a valid footer but with different or several ids.
2666 # Doing this automatically is non-trivial as we don't want to lose
2667 # existing other footers, yet we want to append just 1 desired
2668 # Change-Id. Thus, just create a new footer, but let user verify the
2669 # new description.
2670 message = '%s\n\nChange-Id: %s' % (message, change_id)
2671 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002672 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002673 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002674 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002675 'Please, check the proposed correction to the description, '
2676 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2677 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2678 change_id))
2679 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2680 if not options.force:
2681 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002682 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002683 message = change_desc.description
2684 if not message:
2685 DieWithError("Description is empty. Aborting...")
2686 # Continue the while loop.
2687 # Sanity check of this code - we should end up with proper message
2688 # footer.
2689 assert [change_id] == git_footers.get_footer_change_id(message)
2690 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002691 else: # if not self.GetIssue()
2692 if options.message:
2693 message = options.message
2694 else:
2695 message = CreateDescriptionFromLog(args)
2696 if options.title:
2697 message = options.title + '\n\n' + message
2698 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002699 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002700 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002701 # On first upload, patchset title is always this string, while
2702 # --title flag gets converted to first line of message.
2703 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002704 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002705 if not change_desc.description:
2706 DieWithError("Description is empty. Aborting...")
2707 message = change_desc.description
2708 change_ids = git_footers.get_footer_change_id(message)
2709 if len(change_ids) > 1:
2710 DieWithError('too many Change-Id footers, at most 1 allowed.')
2711 if not change_ids:
2712 # Generate the Change-Id automatically.
2713 message = git_footers.add_footer_change_id(
2714 message, GenerateGerritChangeId(message))
2715 change_desc.set_description(message)
2716 change_ids = git_footers.get_footer_change_id(message)
2717 assert len(change_ids) == 1
2718 change_id = change_ids[0]
2719
2720 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2721 if remote is '.':
2722 # If our upstream branch is local, we base our squashed commit on its
2723 # squashed version.
2724 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2725 # Check the squashed hash of the parent.
2726 parent = RunGit(['config',
2727 'branch.%s.gerritsquashhash' % upstream_branch_name],
2728 error_ok=True).strip()
2729 # Verify that the upstream branch has been uploaded too, otherwise
2730 # Gerrit will create additional CLs when uploading.
2731 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2732 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002733 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002734 '\nUpload upstream branch %s first.\n'
2735 'It is likely that this branch has been rebased since its last '
2736 'upload, so you just need to upload it again.\n'
2737 '(If you uploaded it with --no-squash, then branch dependencies '
2738 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002739 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002740 else:
2741 parent = self.GetCommonAncestorWithUpstream()
2742
2743 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2744 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2745 '-m', message]).strip()
2746 else:
2747 change_desc = ChangeDescription(
2748 options.message or CreateDescriptionFromLog(args))
2749 if not change_desc.description:
2750 DieWithError("Description is empty. Aborting...")
2751
2752 if not git_footers.get_footer_change_id(change_desc.description):
2753 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002754 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2755 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002756 ref_to_push = 'HEAD'
2757 parent = '%s/%s' % (gerrit_remote, branch)
2758 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2759
2760 assert change_desc
2761 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2762 ref_to_push)]).splitlines()
2763 if len(commits) > 1:
2764 print('WARNING: This will upload %d commits. Run the following command '
2765 'to see which commits will be uploaded: ' % len(commits))
2766 print('git log %s..%s' % (parent, ref_to_push))
2767 print('You can also use `git squash-branch` to squash these into a '
2768 'single commit.')
2769 ask_for_data('About to upload; enter to confirm.')
2770
2771 if options.reviewers or options.tbr_owners:
2772 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2773 change)
2774
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002775 # Extra options that can be specified at push time. Doc:
2776 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2777 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002778 if change_desc.get_reviewers(tbr_only=True):
2779 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2780 refspec_opts.append('l=Code-Review+1')
2781
Aaron Gable9b713dd2016-12-14 16:04:21 -08002782 if title:
2783 if not re.match(r'^[\w ]+$', title):
2784 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002785 if not automatic_title:
2786 print('WARNING: Patchset title may only contain alphanumeric chars '
2787 'and spaces. Cleaned up title:\n%s' % title)
2788 if not options.force:
2789 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002790 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2791 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002792 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002793
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002794 if options.send_mail:
2795 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002796 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002797 refspec_opts.append('notify=ALL')
2798 else:
2799 refspec_opts.append('notify=NONE')
2800
tandrii99a72f22016-08-17 14:33:24 -07002801 reviewers = change_desc.get_reviewers()
2802 if reviewers:
2803 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002804
agablec6787972016-09-09 16:13:34 -07002805 if options.private:
2806 refspec_opts.append('draft')
2807
rmistry9eadede2016-09-19 11:22:43 -07002808 if options.topic:
2809 # Documentation on Gerrit topics is here:
2810 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2811 refspec_opts.append('topic=%s' % options.topic)
2812
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002813 refspec_suffix = ''
2814 if refspec_opts:
2815 refspec_suffix = '%' + ','.join(refspec_opts)
2816 assert ' ' not in refspec_suffix, (
2817 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002818 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002819
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002820 try:
2821 push_stdout = gclient_utils.CheckCallAndFilter(
2822 ['git', 'push', gerrit_remote, refspec],
2823 print_stdout=True,
2824 # Flush after every line: useful for seeing progress when running as
2825 # recipe.
2826 filter_fn=lambda _: sys.stdout.flush())
2827 except subprocess2.CalledProcessError:
2828 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002829 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002830
2831 if options.squash:
2832 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2833 change_numbers = [m.group(1)
2834 for m in map(regex.match, push_stdout.splitlines())
2835 if m]
2836 if len(change_numbers) != 1:
2837 DieWithError(
2838 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002839 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002840 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002841 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002842
2843 # Add cc's from the CC_LIST and --cc flag (if any).
2844 cc = self.GetCCList().split(',')
2845 if options.cc:
2846 cc.extend(options.cc)
2847 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002848 if change_desc.get_cced():
2849 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002850 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002851 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002852 self._GetGerritHost(), self.GetIssue(), cc,
2853 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002854 return 0
2855
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002856 def _AddChangeIdToCommitMessage(self, options, args):
2857 """Re-commits using the current message, assumes the commit hook is in
2858 place.
2859 """
2860 log_desc = options.message or CreateDescriptionFromLog(args)
2861 git_command = ['commit', '--amend', '-m', log_desc]
2862 RunGit(git_command)
2863 new_log_desc = CreateDescriptionFromLog(args)
2864 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002865 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002866 return new_log_desc
2867 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002868 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002869
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002870 def SetCQState(self, new_state):
2871 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002872 vote_map = {
2873 _CQState.NONE: 0,
2874 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002875 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002876 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002877 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2878 if new_state == _CQState.DRY_RUN:
2879 # Don't spam everybody reviewer/owner.
2880 kwargs['notify'] = 'NONE'
2881 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002882
tandriie113dfd2016-10-11 10:20:12 -07002883 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002884 try:
2885 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002886 except GerritChangeNotExists:
2887 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002888
2889 if data['status'] in ('ABANDONED', 'MERGED'):
2890 return 'CL %s is closed' % self.GetIssue()
2891
2892 def GetTryjobProperties(self, patchset=None):
2893 """Returns dictionary of properties to launch tryjob."""
2894 data = self._GetChangeDetail(['ALL_REVISIONS'])
2895 patchset = int(patchset or self.GetPatchset())
2896 assert patchset
2897 revision_data = None # Pylint wants it to be defined.
2898 for revision_data in data['revisions'].itervalues():
2899 if int(revision_data['_number']) == patchset:
2900 break
2901 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002902 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002903 (patchset, self.GetIssue()))
2904 return {
2905 'patch_issue': self.GetIssue(),
2906 'patch_set': patchset or self.GetPatchset(),
2907 'patch_project': data['project'],
2908 'patch_storage': 'gerrit',
2909 'patch_ref': revision_data['fetch']['http']['ref'],
2910 'patch_repository_url': revision_data['fetch']['http']['url'],
2911 'patch_gerrit_url': self.GetCodereviewServer(),
2912 }
tandriie113dfd2016-10-11 10:20:12 -07002913
tandriide281ae2016-10-12 06:02:30 -07002914 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002915 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002916
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002917
2918_CODEREVIEW_IMPLEMENTATIONS = {
2919 'rietveld': _RietveldChangelistImpl,
2920 'gerrit': _GerritChangelistImpl,
2921}
2922
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002923
iannuccie53c9352016-08-17 14:40:40 -07002924def _add_codereview_issue_select_options(parser, extra=""):
2925 _add_codereview_select_options(parser)
2926
2927 text = ('Operate on this issue number instead of the current branch\'s '
2928 'implicit issue.')
2929 if extra:
2930 text += ' '+extra
2931 parser.add_option('-i', '--issue', type=int, help=text)
2932
2933
2934def _process_codereview_issue_select_options(parser, options):
2935 _process_codereview_select_options(parser, options)
2936 if options.issue is not None and not options.forced_codereview:
2937 parser.error('--issue must be specified with either --rietveld or --gerrit')
2938
2939
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002940def _add_codereview_select_options(parser):
2941 """Appends --gerrit and --rietveld options to force specific codereview."""
2942 parser.codereview_group = optparse.OptionGroup(
2943 parser, 'EXPERIMENTAL! Codereview override options')
2944 parser.add_option_group(parser.codereview_group)
2945 parser.codereview_group.add_option(
2946 '--gerrit', action='store_true',
2947 help='Force the use of Gerrit for codereview')
2948 parser.codereview_group.add_option(
2949 '--rietveld', action='store_true',
2950 help='Force the use of Rietveld for codereview')
2951
2952
2953def _process_codereview_select_options(parser, options):
2954 if options.gerrit and options.rietveld:
2955 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2956 options.forced_codereview = None
2957 if options.gerrit:
2958 options.forced_codereview = 'gerrit'
2959 elif options.rietveld:
2960 options.forced_codereview = 'rietveld'
2961
2962
tandriif9aefb72016-07-01 09:06:51 -07002963def _get_bug_line_values(default_project, bugs):
2964 """Given default_project and comma separated list of bugs, yields bug line
2965 values.
2966
2967 Each bug can be either:
2968 * a number, which is combined with default_project
2969 * string, which is left as is.
2970
2971 This function may produce more than one line, because bugdroid expects one
2972 project per line.
2973
2974 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2975 ['v8:123', 'chromium:789']
2976 """
2977 default_bugs = []
2978 others = []
2979 for bug in bugs.split(','):
2980 bug = bug.strip()
2981 if bug:
2982 try:
2983 default_bugs.append(int(bug))
2984 except ValueError:
2985 others.append(bug)
2986
2987 if default_bugs:
2988 default_bugs = ','.join(map(str, default_bugs))
2989 if default_project:
2990 yield '%s:%s' % (default_project, default_bugs)
2991 else:
2992 yield default_bugs
2993 for other in sorted(others):
2994 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2995 yield other
2996
2997
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002998class ChangeDescription(object):
2999 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003000 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003001 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003002 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003003 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003004
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003005 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003006 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003007
agable@chromium.org42c20792013-09-12 17:34:49 +00003008 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003009 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003010 return '\n'.join(self._description_lines)
3011
3012 def set_description(self, desc):
3013 if isinstance(desc, basestring):
3014 lines = desc.splitlines()
3015 else:
3016 lines = [line.rstrip() for line in desc]
3017 while lines and not lines[0]:
3018 lines.pop(0)
3019 while lines and not lines[-1]:
3020 lines.pop(-1)
3021 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003022
piman@chromium.org336f9122014-09-04 02:16:55 +00003023 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003024 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003025 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003026 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003027 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003028 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003029
agable@chromium.org42c20792013-09-12 17:34:49 +00003030 # Get the set of R= and TBR= lines and remove them from the desciption.
3031 regexp = re.compile(self.R_LINE)
3032 matches = [regexp.match(line) for line in self._description_lines]
3033 new_desc = [l for i, l in enumerate(self._description_lines)
3034 if not matches[i]]
3035 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003036
agable@chromium.org42c20792013-09-12 17:34:49 +00003037 # Construct new unified R= and TBR= lines.
3038 r_names = []
3039 tbr_names = []
3040 for match in matches:
3041 if not match:
3042 continue
3043 people = cleanup_list([match.group(2).strip()])
3044 if match.group(1) == 'TBR':
3045 tbr_names.extend(people)
3046 else:
3047 r_names.extend(people)
3048 for name in r_names:
3049 if name not in reviewers:
3050 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003051 if add_owners_tbr:
3052 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003053 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003054 all_reviewers = set(tbr_names + reviewers)
3055 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3056 all_reviewers)
3057 tbr_names.extend(owners_db.reviewers_for(missing_files,
3058 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003059 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3060 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3061
3062 # Put the new lines in the description where the old first R= line was.
3063 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3064 if 0 <= line_loc < len(self._description_lines):
3065 if new_tbr_line:
3066 self._description_lines.insert(line_loc, new_tbr_line)
3067 if new_r_line:
3068 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003069 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003070 if new_r_line:
3071 self.append_footer(new_r_line)
3072 if new_tbr_line:
3073 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003074
tandriif9aefb72016-07-01 09:06:51 -07003075 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003076 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003077 self.set_description([
3078 '# Enter a description of the change.',
3079 '# This will be displayed on the codereview site.',
3080 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003081 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003082 '--------------------',
3083 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003084
agable@chromium.org42c20792013-09-12 17:34:49 +00003085 regexp = re.compile(self.BUG_LINE)
3086 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003087 prefix = settings.GetBugPrefix()
3088 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3089 for value in values:
3090 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3091 self.append_footer('BUG=%s' % value)
3092
agable@chromium.org42c20792013-09-12 17:34:49 +00003093 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003094 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003095 if not content:
3096 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003097 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003098
3099 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003100 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3101 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003102 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003103 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003104
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003105 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003106 """Adds a footer line to the description.
3107
3108 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3109 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3110 that Gerrit footers are always at the end.
3111 """
3112 parsed_footer_line = git_footers.parse_footer(line)
3113 if parsed_footer_line:
3114 # Line is a gerrit footer in the form: Footer-Key: any value.
3115 # Thus, must be appended observing Gerrit footer rules.
3116 self.set_description(
3117 git_footers.add_footer(self.description,
3118 key=parsed_footer_line[0],
3119 value=parsed_footer_line[1]))
3120 return
3121
3122 if not self._description_lines:
3123 self._description_lines.append(line)
3124 return
3125
3126 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3127 if gerrit_footers:
3128 # git_footers.split_footers ensures that there is an empty line before
3129 # actual (gerrit) footers, if any. We have to keep it that way.
3130 assert top_lines and top_lines[-1] == ''
3131 top_lines, separator = top_lines[:-1], top_lines[-1:]
3132 else:
3133 separator = [] # No need for separator if there are no gerrit_footers.
3134
3135 prev_line = top_lines[-1] if top_lines else ''
3136 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3137 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3138 top_lines.append('')
3139 top_lines.append(line)
3140 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003141
tandrii99a72f22016-08-17 14:33:24 -07003142 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003143 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003144 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003145 reviewers = [match.group(2).strip()
3146 for match in matches
3147 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003148 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003149
bradnelsond975b302016-10-23 12:20:23 -07003150 def get_cced(self):
3151 """Retrieves the list of reviewers."""
3152 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3153 cced = [match.group(2).strip() for match in matches if match]
3154 return cleanup_list(cced)
3155
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003156 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3157 """Updates this commit description given the parent.
3158
3159 This is essentially what Gnumbd used to do.
3160 Consult https://goo.gl/WMmpDe for more details.
3161 """
3162 assert parent_msg # No, orphan branch creation isn't supported.
3163 assert parent_hash
3164 assert dest_ref
3165 parent_footer_map = git_footers.parse_footers(parent_msg)
3166 # This will also happily parse svn-position, which GnumbD is no longer
3167 # supporting. While we'd generate correct footers, the verifier plugin
3168 # installed in Gerrit will block such commit (ie git push below will fail).
3169 parent_position = git_footers.get_position(parent_footer_map)
3170
3171 # Cherry-picks may have last line obscuring their prior footers,
3172 # from git_footers perspective. This is also what Gnumbd did.
3173 cp_line = None
3174 if (self._description_lines and
3175 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3176 cp_line = self._description_lines.pop()
3177
3178 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3179
3180 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3181 # user interference with actual footers we'd insert below.
3182 for i, (k, v) in enumerate(parsed_footers):
3183 if k.startswith('Cr-'):
3184 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3185
3186 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003187 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003188 if parent_position[0] == dest_ref:
3189 # Same branch as parent.
3190 number = int(parent_position[1]) + 1
3191 else:
3192 number = 1 # New branch, and extra lineage.
3193 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3194 int(parent_position[1])))
3195
3196 parsed_footers.append(('Cr-Commit-Position',
3197 '%s@{#%d}' % (dest_ref, number)))
3198 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3199
3200 self._description_lines = top_lines
3201 if cp_line:
3202 self._description_lines.append(cp_line)
3203 if self._description_lines[-1] != '':
3204 self._description_lines.append('') # Ensure footer separator.
3205 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3206
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003207
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003208def get_approving_reviewers(props):
3209 """Retrieves the reviewers that approved a CL from the issue properties with
3210 messages.
3211
3212 Note that the list may contain reviewers that are not committer, thus are not
3213 considered by the CQ.
3214 """
3215 return sorted(
3216 set(
3217 message['sender']
3218 for message in props['messages']
3219 if message['approval'] and message['sender'] in props['reviewers']
3220 )
3221 )
3222
3223
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003224def FindCodereviewSettingsFile(filename='codereview.settings'):
3225 """Finds the given file starting in the cwd and going up.
3226
3227 Only looks up to the top of the repository unless an
3228 'inherit-review-settings-ok' file exists in the root of the repository.
3229 """
3230 inherit_ok_file = 'inherit-review-settings-ok'
3231 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003232 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003233 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3234 root = '/'
3235 while True:
3236 if filename in os.listdir(cwd):
3237 if os.path.isfile(os.path.join(cwd, filename)):
3238 return open(os.path.join(cwd, filename))
3239 if cwd == root:
3240 break
3241 cwd = os.path.dirname(cwd)
3242
3243
3244def LoadCodereviewSettingsFromFile(fileobj):
3245 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003246 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003247
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003248 def SetProperty(name, setting, unset_error_ok=False):
3249 fullname = 'rietveld.' + name
3250 if setting in keyvals:
3251 RunGit(['config', fullname, keyvals[setting]])
3252 else:
3253 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3254
tandrii48df5812016-10-17 03:55:37 -07003255 if not keyvals.get('GERRIT_HOST', False):
3256 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003257 # Only server setting is required. Other settings can be absent.
3258 # In that case, we ignore errors raised during option deletion attempt.
3259 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003260 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003261 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3262 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003263 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003264 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3265 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003266 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003267 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3268 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003269
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003270 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003271 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003272
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003273 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003274 RunGit(['config', 'gerrit.squash-uploads',
3275 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003276
tandrii@chromium.org28253532016-04-14 13:46:56 +00003277 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003278 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003279 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3280
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003281 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003282 # should be of the form
3283 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3284 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003285 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3286 keyvals['ORIGIN_URL_CONFIG']])
3287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003288
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003289def urlretrieve(source, destination):
3290 """urllib is broken for SSL connections via a proxy therefore we
3291 can't use urllib.urlretrieve()."""
3292 with open(destination, 'w') as f:
3293 f.write(urllib2.urlopen(source).read())
3294
3295
ukai@chromium.org712d6102013-11-27 00:52:58 +00003296def hasSheBang(fname):
3297 """Checks fname is a #! script."""
3298 with open(fname) as f:
3299 return f.read(2).startswith('#!')
3300
3301
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003302# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3303def DownloadHooks(*args, **kwargs):
3304 pass
3305
3306
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003307def DownloadGerritHook(force):
3308 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003309
3310 Args:
3311 force: True to update hooks. False to install hooks if not present.
3312 """
3313 if not settings.GetIsGerrit():
3314 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003315 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003316 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3317 if not os.access(dst, os.X_OK):
3318 if os.path.exists(dst):
3319 if not force:
3320 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003321 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003322 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003323 if not hasSheBang(dst):
3324 DieWithError('Not a script: %s\n'
3325 'You need to download from\n%s\n'
3326 'into .git/hooks/commit-msg and '
3327 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003328 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3329 except Exception:
3330 if os.path.exists(dst):
3331 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003332 DieWithError('\nFailed to download hooks.\n'
3333 'You need to download from\n%s\n'
3334 'into .git/hooks/commit-msg and '
3335 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003336
3337
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003338def GetRietveldCodereviewSettingsInteractively():
3339 """Prompt the user for settings."""
3340 server = settings.GetDefaultServerUrl(error_ok=True)
3341 prompt = 'Rietveld server (host[:port])'
3342 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3343 newserver = ask_for_data(prompt + ':')
3344 if not server and not newserver:
3345 newserver = DEFAULT_SERVER
3346 if newserver:
3347 newserver = gclient_utils.UpgradeToHttps(newserver)
3348 if newserver != server:
3349 RunGit(['config', 'rietveld.server', newserver])
3350
3351 def SetProperty(initial, caption, name, is_url):
3352 prompt = caption
3353 if initial:
3354 prompt += ' ("x" to clear) [%s]' % initial
3355 new_val = ask_for_data(prompt + ':')
3356 if new_val == 'x':
3357 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3358 elif new_val:
3359 if is_url:
3360 new_val = gclient_utils.UpgradeToHttps(new_val)
3361 if new_val != initial:
3362 RunGit(['config', 'rietveld.' + name, new_val])
3363
3364 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3365 SetProperty(settings.GetDefaultPrivateFlag(),
3366 'Private flag (rietveld only)', 'private', False)
3367 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3368 'tree-status-url', False)
3369 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3370 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3371 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3372 'run-post-upload-hook', False)
3373
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003374
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003375@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003376def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003377 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003378
tandrii5d0a0422016-09-14 06:24:35 -07003379 print('WARNING: git cl config works for Rietveld only')
3380 # TODO(tandrii): remove this once we switch to Gerrit.
3381 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003382 parser.add_option('--activate-update', action='store_true',
3383 help='activate auto-updating [rietveld] section in '
3384 '.git/config')
3385 parser.add_option('--deactivate-update', action='store_true',
3386 help='deactivate auto-updating [rietveld] section in '
3387 '.git/config')
3388 options, args = parser.parse_args(args)
3389
3390 if options.deactivate_update:
3391 RunGit(['config', 'rietveld.autoupdate', 'false'])
3392 return
3393
3394 if options.activate_update:
3395 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3396 return
3397
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003398 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003399 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003400 return 0
3401
3402 url = args[0]
3403 if not url.endswith('codereview.settings'):
3404 url = os.path.join(url, 'codereview.settings')
3405
3406 # Load code review settings and download hooks (if available).
3407 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3408 return 0
3409
3410
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003411def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003412 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003413 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3414 branch = ShortBranchName(branchref)
3415 _, args = parser.parse_args(args)
3416 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003417 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003418 return RunGit(['config', 'branch.%s.base-url' % branch],
3419 error_ok=False).strip()
3420 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003421 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003422 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3423 error_ok=False).strip()
3424
3425
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003426def color_for_status(status):
3427 """Maps a Changelist status to color, for CMDstatus and other tools."""
3428 return {
3429 'unsent': Fore.RED,
3430 'waiting': Fore.BLUE,
3431 'reply': Fore.YELLOW,
3432 'lgtm': Fore.GREEN,
3433 'commit': Fore.MAGENTA,
3434 'closed': Fore.CYAN,
3435 'error': Fore.WHITE,
3436 }.get(status, Fore.WHITE)
3437
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003438
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003439def get_cl_statuses(changes, fine_grained, max_processes=None):
3440 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003441
3442 If fine_grained is true, this will fetch CL statuses from the server.
3443 Otherwise, simply indicate if there's a matching url for the given branches.
3444
3445 If max_processes is specified, it is used as the maximum number of processes
3446 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3447 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003448
3449 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003450 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003451 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003452 upload.verbosity = 0
3453
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003454 if not changes:
3455 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003456
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003457 if not fine_grained:
3458 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003459 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003460 for cl in changes:
3461 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003462 return
3463
3464 # First, sort out authentication issues.
3465 logging.debug('ensuring credentials exist')
3466 for cl in changes:
3467 cl.EnsureAuthenticated(force=False, refresh=True)
3468
3469 def fetch(cl):
3470 try:
3471 return (cl, cl.GetStatus())
3472 except:
3473 # See http://crbug.com/629863.
3474 logging.exception('failed to fetch status for %s:', cl)
3475 raise
3476
3477 threads_count = len(changes)
3478 if max_processes:
3479 threads_count = max(1, min(threads_count, max_processes))
3480 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3481
3482 pool = ThreadPool(threads_count)
3483 fetched_cls = set()
3484 try:
3485 it = pool.imap_unordered(fetch, changes).__iter__()
3486 while True:
3487 try:
3488 cl, status = it.next(timeout=5)
3489 except multiprocessing.TimeoutError:
3490 break
3491 fetched_cls.add(cl)
3492 yield cl, status
3493 finally:
3494 pool.close()
3495
3496 # Add any branches that failed to fetch.
3497 for cl in set(changes) - fetched_cls:
3498 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003499
rmistry@google.com2dd99862015-06-22 12:22:18 +00003500
3501def upload_branch_deps(cl, args):
3502 """Uploads CLs of local branches that are dependents of the current branch.
3503
3504 If the local branch dependency tree looks like:
3505 test1 -> test2.1 -> test3.1
3506 -> test3.2
3507 -> test2.2 -> test3.3
3508
3509 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3510 run on the dependent branches in this order:
3511 test2.1, test3.1, test3.2, test2.2, test3.3
3512
3513 Note: This function does not rebase your local dependent branches. Use it when
3514 you make a change to the parent branch that will not conflict with its
3515 dependent branches, and you would like their dependencies updated in
3516 Rietveld.
3517 """
3518 if git_common.is_dirty_git_tree('upload-branch-deps'):
3519 return 1
3520
3521 root_branch = cl.GetBranch()
3522 if root_branch is None:
3523 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3524 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003525 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003526 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3527 'patchset dependencies without an uploaded CL.')
3528
3529 branches = RunGit(['for-each-ref',
3530 '--format=%(refname:short) %(upstream:short)',
3531 'refs/heads'])
3532 if not branches:
3533 print('No local branches found.')
3534 return 0
3535
3536 # Create a dictionary of all local branches to the branches that are dependent
3537 # on it.
3538 tracked_to_dependents = collections.defaultdict(list)
3539 for b in branches.splitlines():
3540 tokens = b.split()
3541 if len(tokens) == 2:
3542 branch_name, tracked = tokens
3543 tracked_to_dependents[tracked].append(branch_name)
3544
vapiera7fbd5a2016-06-16 09:17:49 -07003545 print()
3546 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003547 dependents = []
3548 def traverse_dependents_preorder(branch, padding=''):
3549 dependents_to_process = tracked_to_dependents.get(branch, [])
3550 padding += ' '
3551 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003552 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003553 dependents.append(dependent)
3554 traverse_dependents_preorder(dependent, padding)
3555 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003556 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003557
3558 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003559 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003560 return 0
3561
vapiera7fbd5a2016-06-16 09:17:49 -07003562 print('This command will checkout all dependent branches and run '
3563 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003564 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3565
andybons@chromium.org962f9462016-02-03 20:00:42 +00003566 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003567 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003568 args.extend(['-t', 'Updated patchset dependency'])
3569
rmistry@google.com2dd99862015-06-22 12:22:18 +00003570 # Record all dependents that failed to upload.
3571 failures = {}
3572 # Go through all dependents, checkout the branch and upload.
3573 try:
3574 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003575 print()
3576 print('--------------------------------------')
3577 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003578 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003579 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003580 try:
3581 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003582 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003583 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003584 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003585 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003586 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003587 finally:
3588 # Swap back to the original root branch.
3589 RunGit(['checkout', '-q', root_branch])
3590
vapiera7fbd5a2016-06-16 09:17:49 -07003591 print()
3592 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003593 for dependent_branch in dependents:
3594 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003595 print(' %s : %s' % (dependent_branch, upload_status))
3596 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003597
3598 return 0
3599
3600
kmarshall3bff56b2016-06-06 18:31:47 -07003601def CMDarchive(parser, args):
3602 """Archives and deletes branches associated with closed changelists."""
3603 parser.add_option(
3604 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003605 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003606 parser.add_option(
3607 '-f', '--force', action='store_true',
3608 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003609 parser.add_option(
3610 '-d', '--dry-run', action='store_true',
3611 help='Skip the branch tagging and removal steps.')
3612 parser.add_option(
3613 '-t', '--notags', action='store_true',
3614 help='Do not tag archived branches. '
3615 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003616
3617 auth.add_auth_options(parser)
3618 options, args = parser.parse_args(args)
3619 if args:
3620 parser.error('Unsupported args: %s' % ' '.join(args))
3621 auth_config = auth.extract_auth_config_from_options(options)
3622
3623 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3624 if not branches:
3625 return 0
3626
vapiera7fbd5a2016-06-16 09:17:49 -07003627 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003628 changes = [Changelist(branchref=b, auth_config=auth_config)
3629 for b in branches.splitlines()]
3630 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3631 statuses = get_cl_statuses(changes,
3632 fine_grained=True,
3633 max_processes=options.maxjobs)
3634 proposal = [(cl.GetBranch(),
3635 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3636 for cl, status in statuses
3637 if status == 'closed']
3638 proposal.sort()
3639
3640 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003641 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003642 return 0
3643
3644 current_branch = GetCurrentBranch()
3645
vapiera7fbd5a2016-06-16 09:17:49 -07003646 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003647 if options.notags:
3648 for next_item in proposal:
3649 print(' ' + next_item[0])
3650 else:
3651 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3652 for next_item in proposal:
3653 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003654
kmarshall9249e012016-08-23 12:02:16 -07003655 # Quit now on precondition failure or if instructed by the user, either
3656 # via an interactive prompt or by command line flags.
3657 if options.dry_run:
3658 print('\nNo changes were made (dry run).\n')
3659 return 0
3660 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003661 print('You are currently on a branch \'%s\' which is associated with a '
3662 'closed codereview issue, so archive cannot proceed. Please '
3663 'checkout another branch and run this command again.' %
3664 current_branch)
3665 return 1
kmarshall9249e012016-08-23 12:02:16 -07003666 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003667 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3668 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003669 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003670 return 1
3671
3672 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003673 if not options.notags:
3674 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003675 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003676
vapiera7fbd5a2016-06-16 09:17:49 -07003677 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003678
3679 return 0
3680
3681
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003682def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003683 """Show status of changelists.
3684
3685 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003686 - Red not sent for review or broken
3687 - Blue waiting for review
3688 - Yellow waiting for you to reply to review
3689 - Green LGTM'ed
3690 - Magenta in the commit queue
3691 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003692
3693 Also see 'git cl comments'.
3694 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003695 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003696 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003697 parser.add_option('-f', '--fast', action='store_true',
3698 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003699 parser.add_option(
3700 '-j', '--maxjobs', action='store', type=int,
3701 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003702
3703 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003704 _add_codereview_issue_select_options(
3705 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003706 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003707 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003708 if args:
3709 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003710 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003711
iannuccie53c9352016-08-17 14:40:40 -07003712 if options.issue is not None and not options.field:
3713 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003714
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003715 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003716 cl = Changelist(auth_config=auth_config, issue=options.issue,
3717 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003718 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003719 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003720 elif options.field == 'id':
3721 issueid = cl.GetIssue()
3722 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003723 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003724 elif options.field == 'patch':
3725 patchset = cl.GetPatchset()
3726 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003727 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003728 elif options.field == 'status':
3729 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003730 elif options.field == 'url':
3731 url = cl.GetIssueURL()
3732 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003733 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003734 return 0
3735
3736 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3737 if not branches:
3738 print('No local branch found.')
3739 return 0
3740
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003741 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003742 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003743 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003744 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003745 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003746 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003747 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003748
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003749 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003750 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3751 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3752 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003753 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003754 c, status = output.next()
3755 branch_statuses[c.GetBranch()] = status
3756 status = branch_statuses.pop(branch)
3757 url = cl.GetIssueURL()
3758 if url and (not status or status == 'error'):
3759 # The issue probably doesn't exist anymore.
3760 url += ' (broken)'
3761
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003762 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003763 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003764 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003765 color = ''
3766 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003767 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003768 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003769 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003770 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003771
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003772
3773 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003774 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003775 print('Current branch: %s' % branch)
3776 for cl in changes:
3777 if cl.GetBranch() == branch:
3778 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003779 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003780 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003781 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003782 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003783 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003784 print('Issue description:')
3785 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003786 return 0
3787
3788
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003789def colorize_CMDstatus_doc():
3790 """To be called once in main() to add colors to git cl status help."""
3791 colors = [i for i in dir(Fore) if i[0].isupper()]
3792
3793 def colorize_line(line):
3794 for color in colors:
3795 if color in line.upper():
3796 # Extract whitespaces first and the leading '-'.
3797 indent = len(line) - len(line.lstrip(' ')) + 1
3798 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3799 return line
3800
3801 lines = CMDstatus.__doc__.splitlines()
3802 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3803
3804
phajdan.jre328cf92016-08-22 04:12:17 -07003805def write_json(path, contents):
3806 with open(path, 'w') as f:
3807 json.dump(contents, f)
3808
3809
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003810@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003811def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003812 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003813
3814 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003815 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003816 parser.add_option('-r', '--reverse', action='store_true',
3817 help='Lookup the branch(es) for the specified issues. If '
3818 'no issues are specified, all branches with mapped '
3819 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003820 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003821 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003822 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003823 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003824
dnj@chromium.org406c4402015-03-03 17:22:28 +00003825 if options.reverse:
3826 branches = RunGit(['for-each-ref', 'refs/heads',
3827 '--format=%(refname:short)']).splitlines()
3828
3829 # Reverse issue lookup.
3830 issue_branch_map = {}
3831 for branch in branches:
3832 cl = Changelist(branchref=branch)
3833 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3834 if not args:
3835 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003836 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003837 for issue in args:
3838 if not issue:
3839 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003840 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003841 print('Branch for issue number %s: %s' % (
3842 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003843 if options.json:
3844 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003845 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003846 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003847 if len(args) > 0:
3848 try:
3849 issue = int(args[0])
3850 except ValueError:
3851 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003852 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003853 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003854 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003855 if options.json:
3856 write_json(options.json, {
3857 'issue': cl.GetIssue(),
3858 'issue_url': cl.GetIssueURL(),
3859 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003860 return 0
3861
3862
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003863def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003864 """Shows or posts review comments for any changelist."""
3865 parser.add_option('-a', '--add-comment', dest='comment',
3866 help='comment to add to an issue')
3867 parser.add_option('-i', dest='issue',
3868 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003869 parser.add_option('-j', '--json-file',
3870 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003871 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003872 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003873 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003874
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003875 issue = None
3876 if options.issue:
3877 try:
3878 issue = int(options.issue)
3879 except ValueError:
3880 DieWithError('A review issue id is expected to be a number')
3881
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003882 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003883
3884 if options.comment:
3885 cl.AddComment(options.comment)
3886 return 0
3887
3888 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003889 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003890 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003891 summary.append({
3892 'date': message['date'],
3893 'lgtm': False,
3894 'message': message['text'],
3895 'not_lgtm': False,
3896 'sender': message['sender'],
3897 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003898 if message['disapproval']:
3899 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003900 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003901 elif message['approval']:
3902 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003903 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003904 elif message['sender'] == data['owner_email']:
3905 color = Fore.MAGENTA
3906 else:
3907 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003908 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003909 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003910 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003911 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003912 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003913 if options.json_file:
3914 with open(options.json_file, 'wb') as f:
3915 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003916 return 0
3917
3918
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003919@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003920def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003921 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003922 parser.add_option('-d', '--display', action='store_true',
3923 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003924 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003925 help='New description to set for this issue (- for stdin, '
3926 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003927 parser.add_option('-f', '--force', action='store_true',
3928 help='Delete any unpublished Gerrit edits for this issue '
3929 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003930
3931 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003932 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003933 options, args = parser.parse_args(args)
3934 _process_codereview_select_options(parser, options)
3935
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003936 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003937 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003938 target_issue_arg = ParseIssueNumberArgument(args[0])
3939 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003940 parser.print_help()
3941 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003942
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003943 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003944
martiniss6eda05f2016-06-30 10:18:35 -07003945 kwargs = {
3946 'auth_config': auth_config,
3947 'codereview': options.forced_codereview,
3948 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003949 if target_issue_arg:
3950 kwargs['issue'] = target_issue_arg.issue
3951 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07003952
3953 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003954
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003955 if not cl.GetIssue():
3956 DieWithError('This branch has no associated changelist.')
3957 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003958
smut@google.com34fb6b12015-07-13 20:03:26 +00003959 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003960 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003961 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003962
3963 if options.new_description:
3964 text = options.new_description
3965 if text == '-':
3966 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003967 elif text == '+':
3968 base_branch = cl.GetCommonAncestorWithUpstream()
3969 change = cl.GetChange(base_branch, None, local_description=True)
3970 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003971
3972 description.set_description(text)
3973 else:
3974 description.prompt()
3975
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003976 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003977 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003978 return 0
3979
3980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003981def CreateDescriptionFromLog(args):
3982 """Pulls out the commit log to use as a base for the CL description."""
3983 log_args = []
3984 if len(args) == 1 and not args[0].endswith('.'):
3985 log_args = [args[0] + '..']
3986 elif len(args) == 1 and args[0].endswith('...'):
3987 log_args = [args[0][:-1]]
3988 elif len(args) == 2:
3989 log_args = [args[0] + '..' + args[1]]
3990 else:
3991 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003992 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003993
3994
thestig@chromium.org44202a22014-03-11 19:22:18 +00003995def CMDlint(parser, args):
3996 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003997 parser.add_option('--filter', action='append', metavar='-x,+y',
3998 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003999 auth.add_auth_options(parser)
4000 options, args = parser.parse_args(args)
4001 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004002
4003 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004004 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004005 try:
4006 import cpplint
4007 import cpplint_chromium
4008 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004009 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004010 return 1
4011
4012 # Change the current working directory before calling lint so that it
4013 # shows the correct base.
4014 previous_cwd = os.getcwd()
4015 os.chdir(settings.GetRoot())
4016 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004017 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004018 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4019 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004020 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004021 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004022 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004023
4024 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004025 command = args + files
4026 if options.filter:
4027 command = ['--filter=' + ','.join(options.filter)] + command
4028 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004029
4030 white_regex = re.compile(settings.GetLintRegex())
4031 black_regex = re.compile(settings.GetLintIgnoreRegex())
4032 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4033 for filename in filenames:
4034 if white_regex.match(filename):
4035 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004036 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004037 else:
4038 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4039 extra_check_functions)
4040 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004041 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004042 finally:
4043 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004044 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004045 if cpplint._cpplint_state.error_count != 0:
4046 return 1
4047 return 0
4048
4049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004050def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004051 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004052 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004053 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004054 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004055 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004056 auth.add_auth_options(parser)
4057 options, args = parser.parse_args(args)
4058 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004059
sbc@chromium.org71437c02015-04-09 19:29:40 +00004060 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004061 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004062 return 1
4063
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004064 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004065 if args:
4066 base_branch = args[0]
4067 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004068 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004069 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004070
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004071 cl.RunHook(
4072 committing=not options.upload,
4073 may_prompt=False,
4074 verbose=options.verbose,
4075 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004076 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004077
4078
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004079def GenerateGerritChangeId(message):
4080 """Returns Ixxxxxx...xxx change id.
4081
4082 Works the same way as
4083 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4084 but can be called on demand on all platforms.
4085
4086 The basic idea is to generate git hash of a state of the tree, original commit
4087 message, author/committer info and timestamps.
4088 """
4089 lines = []
4090 tree_hash = RunGitSilent(['write-tree'])
4091 lines.append('tree %s' % tree_hash.strip())
4092 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4093 if code == 0:
4094 lines.append('parent %s' % parent.strip())
4095 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4096 lines.append('author %s' % author.strip())
4097 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4098 lines.append('committer %s' % committer.strip())
4099 lines.append('')
4100 # Note: Gerrit's commit-hook actually cleans message of some lines and
4101 # whitespace. This code is not doing this, but it clearly won't decrease
4102 # entropy.
4103 lines.append(message)
4104 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4105 stdin='\n'.join(lines))
4106 return 'I%s' % change_hash.strip()
4107
4108
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004109def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004110 """Computes the remote branch ref to use for the CL.
4111
4112 Args:
4113 remote (str): The git remote for the CL.
4114 remote_branch (str): The git remote branch for the CL.
4115 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004116 """
4117 if not (remote and remote_branch):
4118 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004119
wittman@chromium.org455dc922015-01-26 20:15:50 +00004120 if target_branch:
4121 # Cannonicalize branch references to the equivalent local full symbolic
4122 # refs, which are then translated into the remote full symbolic refs
4123 # below.
4124 if '/' not in target_branch:
4125 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4126 else:
4127 prefix_replacements = (
4128 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4129 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4130 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4131 )
4132 match = None
4133 for regex, replacement in prefix_replacements:
4134 match = re.search(regex, target_branch)
4135 if match:
4136 remote_branch = target_branch.replace(match.group(0), replacement)
4137 break
4138 if not match:
4139 # This is a branch path but not one we recognize; use as-is.
4140 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004141 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4142 # Handle the refs that need to land in different refs.
4143 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004144
wittman@chromium.org455dc922015-01-26 20:15:50 +00004145 # Create the true path to the remote branch.
4146 # Does the following translation:
4147 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4148 # * refs/remotes/origin/master -> refs/heads/master
4149 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4150 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4151 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4152 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4153 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4154 'refs/heads/')
4155 elif remote_branch.startswith('refs/remotes/branch-heads'):
4156 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004157
wittman@chromium.org455dc922015-01-26 20:15:50 +00004158 return remote_branch
4159
4160
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004161def cleanup_list(l):
4162 """Fixes a list so that comma separated items are put as individual items.
4163
4164 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4165 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4166 """
4167 items = sum((i.split(',') for i in l), [])
4168 stripped_items = (i.strip() for i in items)
4169 return sorted(filter(None, stripped_items))
4170
4171
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004172@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004173def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004174 """Uploads the current changelist to codereview.
4175
4176 Can skip dependency patchset uploads for a branch by running:
4177 git config branch.branch_name.skip-deps-uploads True
4178 To unset run:
4179 git config --unset branch.branch_name.skip-deps-uploads
4180 Can also set the above globally by using the --global flag.
4181 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004182 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4183 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004184 parser.add_option('--bypass-watchlists', action='store_true',
4185 dest='bypass_watchlists',
4186 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004187 parser.add_option('-f', action='store_true', dest='force',
4188 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004189 parser.add_option('--message', '-m', dest='message',
4190 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004191 parser.add_option('-b', '--bug',
4192 help='pre-populate the bug number(s) for this issue. '
4193 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004194 parser.add_option('--message-file', dest='message_file',
4195 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004196 parser.add_option('--title', '-t', dest='title',
4197 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004198 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004199 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004200 help='reviewer email addresses')
4201 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004202 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004203 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004204 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004205 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004206 parser.add_option('--emulate_svn_auto_props',
4207 '--emulate-svn-auto-props',
4208 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004209 dest="emulate_svn_auto_props",
4210 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004211 parser.add_option('-c', '--use-commit-queue', action='store_true',
4212 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004213 parser.add_option('--private', action='store_true',
4214 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004215 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004216 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004217 metavar='TARGET',
4218 help='Apply CL to remote ref TARGET. ' +
4219 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004220 parser.add_option('--squash', action='store_true',
4221 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004222 parser.add_option('--no-squash', action='store_true',
4223 help='Don\'t squash multiple commits into one ' +
4224 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004225 parser.add_option('--topic', default=None,
4226 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004227 parser.add_option('--email', default=None,
4228 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004229 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4230 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004231 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4232 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004233 help='Send the patchset to do a CQ dry run right after '
4234 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004235 parser.add_option('--dependencies', action='store_true',
4236 help='Uploads CLs of all the local branches that depend on '
4237 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004238
rmistry@google.com2dd99862015-06-22 12:22:18 +00004239 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004240 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004241 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004242 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004243 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004244 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004245 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004246
sbc@chromium.org71437c02015-04-09 19:29:40 +00004247 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004248 return 1
4249
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004250 options.reviewers = cleanup_list(options.reviewers)
4251 options.cc = cleanup_list(options.cc)
4252
tandriib80458a2016-06-23 12:20:07 -07004253 if options.message_file:
4254 if options.message:
4255 parser.error('only one of --message and --message-file allowed.')
4256 options.message = gclient_utils.FileRead(options.message_file)
4257 options.message_file = None
4258
tandrii4d0545a2016-07-06 03:56:49 -07004259 if options.cq_dry_run and options.use_commit_queue:
4260 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4261
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004262 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4263 settings.GetIsGerrit()
4264
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004265 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004266 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004267
4268
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004269@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004270def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004271 """DEPRECATED: Used to commit the current changelist via git-svn."""
4272 message = ('git-cl no longer supports committing to SVN repositories via '
4273 'git-svn. You probably want to use `git cl land` instead.')
4274 print(message)
4275 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004276
4277
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004278@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004279def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004280 """Commits the current changelist via git.
4281
4282 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4283 upstream and closes the issue automatically and atomically.
4284
4285 Otherwise (in case of Rietveld):
4286 Squashes branch into a single commit.
4287 Updates commit message with metadata (e.g. pointer to review).
4288 Pushes the code upstream.
4289 Updates review and closes.
4290 """
4291 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4292 help='bypass upload presubmit hook')
4293 parser.add_option('-m', dest='message',
4294 help="override review description")
4295 parser.add_option('-f', action='store_true', dest='force',
4296 help="force yes to questions (don't prompt)")
4297 parser.add_option('-c', dest='contributor',
4298 help="external contributor for patch (appended to " +
4299 "description and used as author for git). Should be " +
4300 "formatted as 'First Last <email@example.com>'")
4301 add_git_similarity(parser)
4302 auth.add_auth_options(parser)
4303 (options, args) = parser.parse_args(args)
4304 auth_config = auth.extract_auth_config_from_options(options)
4305
4306 cl = Changelist(auth_config=auth_config)
4307
4308 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4309 if cl.IsGerrit():
4310 if options.message:
4311 # This could be implemented, but it requires sending a new patch to
4312 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4313 # Besides, Gerrit has the ability to change the commit message on submit
4314 # automatically, thus there is no need to support this option (so far?).
4315 parser.error('-m MESSAGE option is not supported for Gerrit.')
4316 if options.contributor:
4317 parser.error(
4318 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4319 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4320 'the contributor\'s "name <email>". If you can\'t upload such a '
4321 'commit for review, contact your repository admin and request'
4322 '"Forge-Author" permission.')
4323 if not cl.GetIssue():
4324 DieWithError('You must upload the change first to Gerrit.\n'
4325 ' If you would rather have `git cl land` upload '
4326 'automatically for you, see http://crbug.com/642759')
4327 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4328 options.verbose)
4329
4330 current = cl.GetBranch()
4331 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4332 if remote == '.':
4333 print()
4334 print('Attempting to push branch %r into another local branch!' % current)
4335 print()
4336 print('Either reparent this branch on top of origin/master:')
4337 print(' git reparent-branch --root')
4338 print()
4339 print('OR run `git rebase-update` if you think the parent branch is ')
4340 print('already committed.')
4341 print()
4342 print(' Current parent: %r' % upstream_branch)
4343 return 1
4344
4345 if not args:
4346 # Default to merging against our best guess of the upstream branch.
4347 args = [cl.GetUpstreamBranch()]
4348
4349 if options.contributor:
4350 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4351 print("Please provide contibutor as 'First Last <email@example.com>'")
4352 return 1
4353
4354 base_branch = args[0]
4355
4356 if git_common.is_dirty_git_tree('land'):
4357 return 1
4358
4359 # This rev-list syntax means "show all commits not in my branch that
4360 # are in base_branch".
4361 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4362 base_branch]).splitlines()
4363 if upstream_commits:
4364 print('Base branch "%s" has %d commits '
4365 'not in this branch.' % (base_branch, len(upstream_commits)))
4366 print('Run "git merge %s" before attempting to land.' % base_branch)
4367 return 1
4368
4369 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4370 if not options.bypass_hooks:
4371 author = None
4372 if options.contributor:
4373 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4374 hook_results = cl.RunHook(
4375 committing=True,
4376 may_prompt=not options.force,
4377 verbose=options.verbose,
4378 change=cl.GetChange(merge_base, author))
4379 if not hook_results.should_continue():
4380 return 1
4381
4382 # Check the tree status if the tree status URL is set.
4383 status = GetTreeStatus()
4384 if 'closed' == status:
4385 print('The tree is closed. Please wait for it to reopen. Use '
4386 '"git cl land --bypass-hooks" to commit on a closed tree.')
4387 return 1
4388 elif 'unknown' == status:
4389 print('Unable to determine tree status. Please verify manually and '
4390 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4391 return 1
4392
4393 change_desc = ChangeDescription(options.message)
4394 if not change_desc.description and cl.GetIssue():
4395 change_desc = ChangeDescription(cl.GetDescription())
4396
4397 if not change_desc.description:
4398 if not cl.GetIssue() and options.bypass_hooks:
4399 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4400 else:
4401 print('No description set.')
4402 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4403 return 1
4404
4405 # Keep a separate copy for the commit message, because the commit message
4406 # contains the link to the Rietveld issue, while the Rietveld message contains
4407 # the commit viewvc url.
4408 if cl.GetIssue():
4409 change_desc.update_reviewers(cl.GetApprovingReviewers())
4410
4411 commit_desc = ChangeDescription(change_desc.description)
4412 if cl.GetIssue():
4413 # Xcode won't linkify this URL unless there is a non-whitespace character
4414 # after it. Add a period on a new line to circumvent this. Also add a space
4415 # before the period to make sure that Gitiles continues to correctly resolve
4416 # the URL.
4417 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4418 if options.contributor:
4419 commit_desc.append_footer('Patch from %s.' % options.contributor)
4420
4421 print('Description:')
4422 print(commit_desc.description)
4423
4424 branches = [merge_base, cl.GetBranchRef()]
4425 if not options.force:
4426 print_stats(options.similarity, options.find_copies, branches)
4427
4428 # We want to squash all this branch's commits into one commit with the proper
4429 # description. We do this by doing a "reset --soft" to the base branch (which
4430 # keeps the working copy the same), then landing that.
4431 MERGE_BRANCH = 'git-cl-commit'
4432 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4433 # Delete the branches if they exist.
4434 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4435 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4436 result = RunGitWithCode(showref_cmd)
4437 if result[0] == 0:
4438 RunGit(['branch', '-D', branch])
4439
4440 # We might be in a directory that's present in this branch but not in the
4441 # trunk. Move up to the top of the tree so that git commands that expect a
4442 # valid CWD won't fail after we check out the merge branch.
4443 rel_base_path = settings.GetRelativeRoot()
4444 if rel_base_path:
4445 os.chdir(rel_base_path)
4446
4447 # Stuff our change into the merge branch.
4448 # We wrap in a try...finally block so if anything goes wrong,
4449 # we clean up the branches.
4450 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004451 revision = None
4452 try:
4453 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4454 RunGit(['reset', '--soft', merge_base])
4455 if options.contributor:
4456 RunGit(
4457 [
4458 'commit', '--author', options.contributor,
4459 '-m', commit_desc.description,
4460 ])
4461 else:
4462 RunGit(['commit', '-m', commit_desc.description])
4463
4464 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4465 mirror = settings.GetGitMirror(remote)
4466 if mirror:
4467 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004468 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004469 else:
4470 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004471 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004472 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4473
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004474 if git_numberer_enabled:
4475 # TODO(tandrii): maybe do autorebase + retry on failure
4476 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004477 logging.debug('Adding git number footers')
4478 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4479 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4480 branch)
4481 # Ensure timestamps are monotonically increasing.
4482 timestamp = max(1 + _get_committer_timestamp(merge_base),
4483 _get_committer_timestamp('HEAD'))
4484 _git_amend_head(commit_desc.description, timestamp)
4485 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004486
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004487 retcode, output = RunGitWithCode(
4488 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004489 if retcode == 0:
4490 revision = RunGit(['rev-parse', 'HEAD']).strip()
4491 logging.debug(output)
4492 except: # pylint: disable=bare-except
4493 if _IS_BEING_TESTED:
4494 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4495 + '-' * 30 + '8<' + '-' * 30)
4496 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4497 raise
4498 finally:
4499 # And then swap back to the original branch and clean up.
4500 RunGit(['checkout', '-q', cl.GetBranch()])
4501 RunGit(['branch', '-D', MERGE_BRANCH])
4502
4503 if not revision:
4504 print('Failed to push. If this persists, please file a bug.')
4505 return 1
4506
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004507 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004508 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004509 if viewvc_url and revision:
4510 change_desc.append_footer(
4511 'Committed: %s%s' % (viewvc_url, revision))
4512 elif revision:
4513 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004514 print('Closing issue '
4515 '(you may be prompted for your codereview password)...')
4516 cl.UpdateDescription(change_desc.description)
4517 cl.CloseIssue()
4518 props = cl.GetIssueProperties()
4519 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004520 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4521 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004522 if options.bypass_hooks:
4523 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4524 else:
4525 comment += ' (presubmit successful).'
4526 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4527
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004528 if os.path.isfile(POSTUPSTREAM_HOOK):
4529 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4530
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004531 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004532
4533
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004534@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004535def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004536 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537 parser.add_option('-b', dest='newbranch',
4538 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004539 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004540 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004541 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4542 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004543 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004544 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004545 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004546 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004547 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004548 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004549
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004550
4551 group = optparse.OptionGroup(
4552 parser,
4553 'Options for continuing work on the current issue uploaded from a '
4554 'different clone (e.g. different machine). Must be used independently '
4555 'from the other options. No issue number should be specified, and the '
4556 'branch must have an issue number associated with it')
4557 group.add_option('--reapply', action='store_true', dest='reapply',
4558 help='Reset the branch and reapply the issue.\n'
4559 'CAUTION: This will undo any local changes in this '
4560 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004561
4562 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004563 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004564 parser.add_option_group(group)
4565
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004566 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004567 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004568 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004569 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004570 auth_config = auth.extract_auth_config_from_options(options)
4571
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004572
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004573 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004574 if options.newbranch:
4575 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004576 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004577 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004578
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004579 cl = Changelist(auth_config=auth_config,
4580 codereview=options.forced_codereview)
4581 if not cl.GetIssue():
4582 parser.error('current branch must have an associated issue')
4583
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004584 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004585 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004586 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004587
4588 RunGit(['reset', '--hard', upstream])
4589 if options.pull:
4590 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004591
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004592 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4593 options.directory)
4594
4595 if len(args) != 1 or not args[0]:
4596 parser.error('Must specify issue number or url')
4597
4598 # We don't want uncommitted changes mixed up with the patch.
4599 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004600 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004601
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004602 if options.newbranch:
4603 if options.force:
4604 RunGit(['branch', '-D', options.newbranch],
4605 stderr=subprocess2.PIPE, error_ok=True)
4606 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004607 elif not GetCurrentBranch():
4608 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004609
4610 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4611
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004612 if cl.IsGerrit():
4613 if options.reject:
4614 parser.error('--reject is not supported with Gerrit codereview.')
4615 if options.nocommit:
4616 parser.error('--nocommit is not supported with Gerrit codereview.')
4617 if options.directory:
4618 parser.error('--directory is not supported with Gerrit codereview.')
4619
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004620 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004621 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004622
4623
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004624def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004625 """Fetches the tree status and returns either 'open', 'closed',
4626 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004627 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004628 if url:
4629 status = urllib2.urlopen(url).read().lower()
4630 if status.find('closed') != -1 or status == '0':
4631 return 'closed'
4632 elif status.find('open') != -1 or status == '1':
4633 return 'open'
4634 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004635 return 'unset'
4636
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004637
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004638def GetTreeStatusReason():
4639 """Fetches the tree status from a json url and returns the message
4640 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004641 url = settings.GetTreeStatusUrl()
4642 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004643 connection = urllib2.urlopen(json_url)
4644 status = json.loads(connection.read())
4645 connection.close()
4646 return status['message']
4647
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004648
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004649def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004650 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004651 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004652 status = GetTreeStatus()
4653 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004654 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004655 return 2
4656
vapiera7fbd5a2016-06-16 09:17:49 -07004657 print('The tree is %s' % status)
4658 print()
4659 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004660 if status != 'open':
4661 return 1
4662 return 0
4663
4664
maruel@chromium.org15192402012-09-06 12:38:29 +00004665def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004666 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004667 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004668 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004669 '-b', '--bot', action='append',
4670 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4671 'times to specify multiple builders. ex: '
4672 '"-b win_rel -b win_layout". See '
4673 'the try server waterfall for the builders name and the tests '
4674 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004675 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004676 '-B', '--bucket', default='',
4677 help=('Buildbucket bucket to send the try requests.'))
4678 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004679 '-m', '--master', default='',
4680 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004681 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004682 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004683 help='Revision to use for the try job; default: the revision will '
4684 'be determined by the try recipe that builder runs, which usually '
4685 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004686 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004687 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004688 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004689 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004690 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004691 '--project',
4692 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004693 'in recipe to determine to which repository or directory to '
4694 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004695 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004696 '-p', '--property', dest='properties', action='append', default=[],
4697 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004698 'key2=value2 etc. The value will be treated as '
4699 'json if decodable, or as string otherwise. '
4700 'NOTE: using this may make your try job not usable for CQ, '
4701 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004702 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004703 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4704 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004705 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004706 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004707 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004708 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004709
machenbach@chromium.org45453142015-09-15 08:45:22 +00004710 # Make sure that all properties are prop=value pairs.
4711 bad_params = [x for x in options.properties if '=' not in x]
4712 if bad_params:
4713 parser.error('Got properties with missing "=": %s' % bad_params)
4714
maruel@chromium.org15192402012-09-06 12:38:29 +00004715 if args:
4716 parser.error('Unknown arguments: %s' % args)
4717
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004718 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004719 if not cl.GetIssue():
4720 parser.error('Need to upload first')
4721
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004722 if cl.IsGerrit():
4723 # HACK: warm up Gerrit change detail cache to save on RPCs.
4724 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4725
tandriie113dfd2016-10-11 10:20:12 -07004726 error_message = cl.CannotTriggerTryJobReason()
4727 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004728 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004729
borenet6c0efe62016-10-19 08:13:29 -07004730 if options.bucket and options.master:
4731 parser.error('Only one of --bucket and --master may be used.')
4732
qyearsley1fdfcb62016-10-24 13:22:03 -07004733 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004734
qyearsleydd49f942016-10-28 11:57:22 -07004735 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4736 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004737 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004738 if options.verbose:
4739 print('git cl try with no bots now defaults to CQ Dry Run.')
4740 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004741
borenet6c0efe62016-10-19 08:13:29 -07004742 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004743 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004744 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004745 'of bot requires an initial job from a parent (usually a builder). '
4746 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004747 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004748 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004749
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004750 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004751 # TODO(tandrii): Checking local patchset against remote patchset is only
4752 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4753 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004754 print('Warning: Codereview server has newer patchsets (%s) than most '
4755 'recent upload from local checkout (%s). Did a previous upload '
4756 'fail?\n'
4757 'By default, git cl try uses the latest patchset from '
4758 'codereview, continuing to use patchset %s.\n' %
4759 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004760
tandrii568043b2016-10-11 07:49:18 -07004761 try:
borenet6c0efe62016-10-19 08:13:29 -07004762 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4763 patchset)
tandrii568043b2016-10-11 07:49:18 -07004764 except BuildbucketResponseException as ex:
4765 print('ERROR: %s' % ex)
4766 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004767 return 0
4768
4769
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004770def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004771 """Prints info about try jobs associated with current CL."""
4772 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004773 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004774 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004775 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004776 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004777 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004778 '--color', action='store_true', default=setup_color.IS_TTY,
4779 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004780 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004781 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4782 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004783 group.add_option(
4784 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004785 parser.add_option_group(group)
4786 auth.add_auth_options(parser)
4787 options, args = parser.parse_args(args)
4788 if args:
4789 parser.error('Unrecognized args: %s' % ' '.join(args))
4790
4791 auth_config = auth.extract_auth_config_from_options(options)
4792 cl = Changelist(auth_config=auth_config)
4793 if not cl.GetIssue():
4794 parser.error('Need to upload first')
4795
tandrii221ab252016-10-06 08:12:04 -07004796 patchset = options.patchset
4797 if not patchset:
4798 patchset = cl.GetMostRecentPatchset()
4799 if not patchset:
4800 parser.error('Codereview doesn\'t know about issue %s. '
4801 'No access to issue or wrong issue number?\n'
4802 'Either upload first, or pass --patchset explicitely' %
4803 cl.GetIssue())
4804
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004805 # TODO(tandrii): Checking local patchset against remote patchset is only
4806 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4807 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004808 print('Warning: Codereview server has newer patchsets (%s) than most '
4809 'recent upload from local checkout (%s). Did a previous upload '
4810 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004811 'By default, git cl try-results uses the latest patchset from '
4812 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004813 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004814 try:
tandrii221ab252016-10-06 08:12:04 -07004815 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004816 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004817 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004818 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004819 if options.json:
4820 write_try_results_json(options.json, jobs)
4821 else:
4822 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004823 return 0
4824
4825
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004826@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004827def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004828 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004829 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004830 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004831 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004832
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004833 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004834 if args:
4835 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004836 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004837 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004838 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004839 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004840
4841 # Clear configured merge-base, if there is one.
4842 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004843 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004844 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004845 return 0
4846
4847
thestig@chromium.org00858c82013-12-02 23:08:03 +00004848def CMDweb(parser, args):
4849 """Opens the current CL in the web browser."""
4850 _, args = parser.parse_args(args)
4851 if args:
4852 parser.error('Unrecognized args: %s' % ' '.join(args))
4853
4854 issue_url = Changelist().GetIssueURL()
4855 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004856 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004857 return 1
4858
4859 webbrowser.open(issue_url)
4860 return 0
4861
4862
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004863def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004864 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004865 parser.add_option('-d', '--dry-run', action='store_true',
4866 help='trigger in dry run mode')
4867 parser.add_option('-c', '--clear', action='store_true',
4868 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004869 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004870 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004871 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004872 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004873 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004874 if args:
4875 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004876 if options.dry_run and options.clear:
4877 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4878
iannuccie53c9352016-08-17 14:40:40 -07004879 cl = Changelist(auth_config=auth_config, issue=options.issue,
4880 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004881 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004882 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004883 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004884 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004885 state = _CQState.DRY_RUN
4886 else:
4887 state = _CQState.COMMIT
4888 if not cl.GetIssue():
4889 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004890 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004891 return 0
4892
4893
groby@chromium.org411034a2013-02-26 15:12:01 +00004894def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004895 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004896 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004897 auth.add_auth_options(parser)
4898 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004899 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004900 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004901 if args:
4902 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004903 cl = Changelist(auth_config=auth_config, issue=options.issue,
4904 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004905 # Ensure there actually is an issue to close.
4906 cl.GetDescription()
4907 cl.CloseIssue()
4908 return 0
4909
4910
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004911def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004912 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004913 parser.add_option(
4914 '--stat',
4915 action='store_true',
4916 dest='stat',
4917 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004918 auth.add_auth_options(parser)
4919 options, args = parser.parse_args(args)
4920 auth_config = auth.extract_auth_config_from_options(options)
4921 if args:
4922 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004923
4924 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004925 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004926 # Staged changes would be committed along with the patch from last
4927 # upload, hence counted toward the "last upload" side in the final
4928 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004929 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004930 return 1
4931
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004932 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004933 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004934 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004935 if not issue:
4936 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004937 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004938 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004939
4940 # Create a new branch based on the merge-base
4941 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004942 # Clear cached branch in cl object, to avoid overwriting original CL branch
4943 # properties.
4944 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004945 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004946 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004947 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004948 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004949 return rtn
4950
wychen@chromium.org06928532015-02-03 02:11:29 +00004951 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004952 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004953 cmd = ['git', 'diff']
4954 if options.stat:
4955 cmd.append('--stat')
4956 cmd.extend([TMP_BRANCH, branch, '--'])
4957 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004958 finally:
4959 RunGit(['checkout', '-q', branch])
4960 RunGit(['branch', '-D', TMP_BRANCH])
4961
4962 return 0
4963
4964
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004965def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004966 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004967 parser.add_option(
4968 '--no-color',
4969 action='store_true',
4970 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004971 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004972 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004973 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004974
4975 author = RunGit(['config', 'user.email']).strip() or None
4976
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004977 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004978
4979 if args:
4980 if len(args) > 1:
4981 parser.error('Unknown args')
4982 base_branch = args[0]
4983 else:
4984 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004985 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004986
4987 change = cl.GetChange(base_branch, None)
4988 return owners_finder.OwnersFinder(
4989 [f.LocalPath() for f in
4990 cl.GetChange(base_branch, None).AffectedFiles()],
4991 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004992 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004993 disable_color=options.no_color).run()
4994
4995
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004996def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004997 """Generates a diff command."""
4998 # Generate diff for the current branch's changes.
4999 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005000 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005001
5002 if args:
5003 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005004 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005005 diff_cmd.append(arg)
5006 else:
5007 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005008
5009 return diff_cmd
5010
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005011
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005012def MatchingFileType(file_name, extensions):
5013 """Returns true if the file name ends with one of the given extensions."""
5014 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005015
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005016
enne@chromium.org555cfe42014-01-29 18:21:39 +00005017@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005018def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005019 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005020 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005021 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005022 parser.add_option('--full', action='store_true',
5023 help='Reformat the full content of all touched files')
5024 parser.add_option('--dry-run', action='store_true',
5025 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005026 parser.add_option('--python', action='store_true',
5027 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005028 parser.add_option('--js', action='store_true',
5029 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005030 parser.add_option('--diff', action='store_true',
5031 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005032 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005033
Daniel Chengc55eecf2016-12-30 03:11:02 -08005034 # Normalize any remaining args against the current path, so paths relative to
5035 # the current directory are still resolved as expected.
5036 args = [os.path.join(os.getcwd(), arg) for arg in args]
5037
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005038 # git diff generates paths against the root of the repository. Change
5039 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005040 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005041 if rel_base_path:
5042 os.chdir(rel_base_path)
5043
digit@chromium.org29e47272013-05-17 17:01:46 +00005044 # Grab the merge-base commit, i.e. the upstream commit of the current
5045 # branch when it was created or the last time it was rebased. This is
5046 # to cover the case where the user may have called "git fetch origin",
5047 # moving the origin branch to a newer commit, but hasn't rebased yet.
5048 upstream_commit = None
5049 cl = Changelist()
5050 upstream_branch = cl.GetUpstreamBranch()
5051 if upstream_branch:
5052 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5053 upstream_commit = upstream_commit.strip()
5054
5055 if not upstream_commit:
5056 DieWithError('Could not find base commit for this branch. '
5057 'Are you in detached state?')
5058
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005059 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5060 diff_output = RunGit(changed_files_cmd)
5061 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005062 # Filter out files deleted by this CL
5063 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005064
Christopher Lamc5ba6922017-01-24 11:19:14 +11005065 if opts.js:
5066 CLANG_EXTS.append('.js')
5067
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005068 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5069 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5070 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005071 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005072
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005073 top_dir = os.path.normpath(
5074 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5075
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005076 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5077 # formatted. This is used to block during the presubmit.
5078 return_value = 0
5079
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005080 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005081 # Locate the clang-format binary in the checkout
5082 try:
5083 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005084 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005085 DieWithError(e)
5086
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005087 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005088 cmd = [clang_format_tool]
5089 if not opts.dry_run and not opts.diff:
5090 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005091 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005092 if opts.diff:
5093 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005094 else:
5095 env = os.environ.copy()
5096 env['PATH'] = str(os.path.dirname(clang_format_tool))
5097 try:
5098 script = clang_format.FindClangFormatScriptInChromiumTree(
5099 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005100 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005101 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005102
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005103 cmd = [sys.executable, script, '-p0']
5104 if not opts.dry_run and not opts.diff:
5105 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005106
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005107 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5108 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005109
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005110 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5111 if opts.diff:
5112 sys.stdout.write(stdout)
5113 if opts.dry_run and len(stdout) > 0:
5114 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005115
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005116 # Similar code to above, but using yapf on .py files rather than clang-format
5117 # on C/C++ files
5118 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005119 yapf_tool = gclient_utils.FindExecutable('yapf')
5120 if yapf_tool is None:
5121 DieWithError('yapf not found in PATH')
5122
5123 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005124 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005125 cmd = [yapf_tool]
5126 if not opts.dry_run and not opts.diff:
5127 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005128 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005129 if opts.diff:
5130 sys.stdout.write(stdout)
5131 else:
5132 # TODO(sbc): yapf --lines mode still has some issues.
5133 # https://github.com/google/yapf/issues/154
5134 DieWithError('--python currently only works with --full')
5135
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005136 # Dart's formatter does not have the nice property of only operating on
5137 # modified chunks, so hard code full.
5138 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005139 try:
5140 command = [dart_format.FindDartFmtToolInChromiumTree()]
5141 if not opts.dry_run and not opts.diff:
5142 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005143 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005144
ppi@chromium.org6593d932016-03-03 15:41:15 +00005145 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005146 if opts.dry_run and stdout:
5147 return_value = 2
5148 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005149 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5150 'found in this checkout. Files in other languages are still '
5151 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005152
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005153 # Format GN build files. Always run on full build files for canonical form.
5154 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005155 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005156 if opts.dry_run or opts.diff:
5157 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005158 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005159 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5160 shell=sys.platform == 'win32',
5161 cwd=top_dir)
5162 if opts.dry_run and gn_ret == 2:
5163 return_value = 2 # Not formatted.
5164 elif opts.diff and gn_ret == 2:
5165 # TODO this should compute and print the actual diff.
5166 print("This change has GN build file diff for " + gn_diff_file)
5167 elif gn_ret != 0:
5168 # For non-dry run cases (and non-2 return values for dry-run), a
5169 # nonzero error code indicates a failure, probably because the file
5170 # doesn't parse.
5171 DieWithError("gn format failed on " + gn_diff_file +
5172 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005173
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005174 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005175
5176
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005177@subcommand.usage('<codereview url or issue id>')
5178def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005179 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005180 _, args = parser.parse_args(args)
5181
5182 if len(args) != 1:
5183 parser.print_help()
5184 return 1
5185
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005186 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005187 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005188 parser.print_help()
5189 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005190 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005191
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005192 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005193 output = RunGit(['config', '--local', '--get-regexp',
5194 r'branch\..*\.%s' % issueprefix],
5195 error_ok=True)
5196 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005197 if issue == target_issue:
5198 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005199
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005200 branches = []
5201 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005202 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005203 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005204 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005205 return 1
5206 if len(branches) == 1:
5207 RunGit(['checkout', branches[0]])
5208 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005209 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005210 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005211 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005212 which = raw_input('Choose by index: ')
5213 try:
5214 RunGit(['checkout', branches[int(which)]])
5215 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005216 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005217 return 1
5218
5219 return 0
5220
5221
maruel@chromium.org29404b52014-09-08 22:58:00 +00005222def CMDlol(parser, args):
5223 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005224 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005225 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5226 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5227 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005228 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005229 return 0
5230
5231
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005232class OptionParser(optparse.OptionParser):
5233 """Creates the option parse and add --verbose support."""
5234 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005235 optparse.OptionParser.__init__(
5236 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005237 self.add_option(
5238 '-v', '--verbose', action='count', default=0,
5239 help='Use 2 times for more debugging info')
5240
5241 def parse_args(self, args=None, values=None):
5242 options, args = optparse.OptionParser.parse_args(self, args, values)
5243 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005244 logging.basicConfig(
5245 level=levels[min(options.verbose, len(levels) - 1)],
5246 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5247 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005248 return options, args
5249
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005250
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005251def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005252 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005253 print('\nYour python version %s is unsupported, please upgrade.\n' %
5254 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005255 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005256
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005257 # Reload settings.
5258 global settings
5259 settings = Settings()
5260
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005261 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005262 dispatcher = subcommand.CommandDispatcher(__name__)
5263 try:
5264 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005265 except auth.AuthenticationError as e:
5266 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005267 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005268 if e.code != 500:
5269 raise
5270 DieWithError(
5271 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5272 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005273 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005274
5275
5276if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005277 # These affect sys.stdout so do it outside of main() to simplify mocks in
5278 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005279 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005280 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005281 try:
5282 sys.exit(main(sys.argv[1:]))
5283 except KeyboardInterrupt:
5284 sys.stderr.write('interrupted\n')
5285 sys.exit(1)