blob: 839d2e556c38d6cc9eedd40d704c46b9cce9e73a [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', [])
2390 if messages:
2391 owner = data['owner'].get('_account_id')
2392 last_message_author = messages[-1].get('author', {}).get('_account_id')
2393 if owner != last_message_author:
2394 # Some reply from non-owner.
2395 return 'reply'
2396
2397 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002398
2399 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002400 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002401 return data['revisions'][data['current_revision']]['_number']
2402
2403 def FetchDescription(self):
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002404 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002405 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002406 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002407
dsansomee2d6fd92016-09-08 00:10:47 -07002408 def UpdateDescriptionRemote(self, description, force=False):
2409 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2410 if not force:
2411 ask_for_data(
2412 'The description cannot be modified while the issue has a pending '
2413 'unpublished edit. Either publish the edit in the Gerrit web UI '
2414 'or delete it.\n\n'
2415 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2416
2417 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2418 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002419 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002420 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002421
2422 def CloseIssue(self):
2423 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2424
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002425 def GetApprovingReviewers(self):
2426 """Returns a list of reviewers approving the change.
2427
2428 Note: not necessarily committers.
2429 """
2430 raise NotImplementedError()
2431
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002432 def SubmitIssue(self, wait_for_merge=True):
2433 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2434 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002435
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002436 def _GetChangeDetail(self, options=None, issue=None,
2437 no_cache=False):
2438 """Returns details of the issue by querying Gerrit and caching results.
2439
2440 If fresh data is needed, set no_cache=True which will clear cache and
2441 thus new data will be fetched from Gerrit.
2442 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002443 options = options or []
2444 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002445 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002446
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002447 # Optimization to avoid multiple RPCs:
2448 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2449 'CURRENT_COMMIT' not in options):
2450 options.append('CURRENT_COMMIT')
2451
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002452 # Normalize issue and options for consistent keys in cache.
2453 issue = str(issue)
2454 options = [o.upper() for o in options]
2455
2456 # Check in cache first unless no_cache is True.
2457 if no_cache:
2458 self._detail_cache.pop(issue, None)
2459 else:
2460 options_set = frozenset(options)
2461 for cached_options_set, data in self._detail_cache.get(issue, []):
2462 # Assumption: data fetched before with extra options is suitable
2463 # for return for a smaller set of options.
2464 # For example, if we cached data for
2465 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2466 # and request is for options=[CURRENT_REVISION],
2467 # THEN we can return prior cached data.
2468 if options_set.issubset(cached_options_set):
2469 return data
2470
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002471 try:
2472 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2473 options, ignore_404=False)
2474 except gerrit_util.GerritError as e:
2475 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002476 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002477 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002478
2479 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002480 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002481
agable32978d92016-11-01 12:55:02 -07002482 def _GetChangeCommit(self, issue=None):
2483 issue = issue or self.GetIssue()
2484 assert issue, 'issue is required to query Gerrit'
2485 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2486 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002487 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002488 return data
2489
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002490 def CMDLand(self, force, bypass_hooks, verbose):
2491 if git_common.is_dirty_git_tree('land'):
2492 return 1
tandriid60367b2016-06-22 05:25:12 -07002493 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2494 if u'Commit-Queue' in detail.get('labels', {}):
2495 if not force:
2496 ask_for_data('\nIt seems this repository has a Commit Queue, '
2497 'which can test and land changes for you. '
2498 'Are you sure you wish to bypass it?\n'
2499 'Press Enter to continue, Ctrl+C to abort.')
2500
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002501 differs = True
tandriic4344b52016-08-29 06:04:54 -07002502 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002503 # Note: git diff outputs nothing if there is no diff.
2504 if not last_upload or RunGit(['diff', last_upload]).strip():
2505 print('WARNING: some changes from local branch haven\'t been uploaded')
2506 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002507 if detail['current_revision'] == last_upload:
2508 differs = False
2509 else:
2510 print('WARNING: local branch contents differ from latest uploaded '
2511 'patchset')
2512 if differs:
2513 if not force:
2514 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002515 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2516 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002517 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2518 elif not bypass_hooks:
2519 hook_results = self.RunHook(
2520 committing=True,
2521 may_prompt=not force,
2522 verbose=verbose,
2523 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2524 if not hook_results.should_continue():
2525 return 1
2526
2527 self.SubmitIssue(wait_for_merge=True)
2528 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002529 links = self._GetChangeCommit().get('web_links', [])
2530 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002531 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002532 print('Landed as %s' % link.get('url'))
2533 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002534 return 0
2535
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002536 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2537 directory):
2538 assert not reject
2539 assert not nocommit
2540 assert not directory
2541 assert parsed_issue_arg.valid
2542
2543 self._changelist.issue = parsed_issue_arg.issue
2544
2545 if parsed_issue_arg.hostname:
2546 self._gerrit_host = parsed_issue_arg.hostname
2547 self._gerrit_server = 'https://%s' % self._gerrit_host
2548
tandriic2405f52016-10-10 08:13:15 -07002549 try:
2550 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002551 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002552 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002553
2554 if not parsed_issue_arg.patchset:
2555 # Use current revision by default.
2556 revision_info = detail['revisions'][detail['current_revision']]
2557 patchset = int(revision_info['_number'])
2558 else:
2559 patchset = parsed_issue_arg.patchset
2560 for revision_info in detail['revisions'].itervalues():
2561 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2562 break
2563 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002564 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002565 (parsed_issue_arg.patchset, self.GetIssue()))
2566
2567 fetch_info = revision_info['fetch']['http']
2568 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2569 RunGit(['cherry-pick', 'FETCH_HEAD'])
2570 self.SetIssue(self.GetIssue())
2571 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002572 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002573 (self.GetIssue(), self.GetPatchset()))
2574 return 0
2575
2576 @staticmethod
2577 def ParseIssueURL(parsed_url):
2578 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2579 return None
2580 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2581 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2582 # Short urls like https://domain/<issue_number> can be used, but don't allow
2583 # specifying the patchset (you'd 404), but we allow that here.
2584 if parsed_url.path == '/':
2585 part = parsed_url.fragment
2586 else:
2587 part = parsed_url.path
2588 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2589 if match:
2590 return _ParsedIssueNumberArgument(
2591 issue=int(match.group(2)),
2592 patchset=int(match.group(4)) if match.group(4) else None,
2593 hostname=parsed_url.netloc)
2594 return None
2595
tandrii16e0b4e2016-06-07 10:34:28 -07002596 def _GerritCommitMsgHookCheck(self, offer_removal):
2597 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2598 if not os.path.exists(hook):
2599 return
2600 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2601 # custom developer made one.
2602 data = gclient_utils.FileRead(hook)
2603 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2604 return
2605 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002606 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002607 'and may interfere with it in subtle ways.\n'
2608 'We recommend you remove the commit-msg hook.')
2609 if offer_removal:
2610 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2611 if reply.lower().startswith('y'):
2612 gclient_utils.rm_file_or_tree(hook)
2613 print('Gerrit commit-msg hook removed.')
2614 else:
2615 print('OK, will keep Gerrit commit-msg hook in place.')
2616
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002617 def CMDUploadChange(self, options, args, change):
2618 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002619 if options.squash and options.no_squash:
2620 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002621
2622 if not options.squash and not options.no_squash:
2623 # Load default for user, repo, squash=true, in this order.
2624 options.squash = settings.GetSquashGerritUploads()
2625 elif options.no_squash:
2626 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002627
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002628 # We assume the remote called "origin" is the one we want.
2629 # It is probably not worthwhile to support different workflows.
2630 gerrit_remote = 'origin'
2631
2632 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002633 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002634
Aaron Gableb56ad332017-01-06 15:24:31 -08002635 # This may be None; default fallback value is determined in logic below.
2636 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002637 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002638
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002639 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002640 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002641 if self.GetIssue():
2642 # Try to get the message from a previous upload.
2643 message = self.GetDescription()
2644 if not message:
2645 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002646 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002647 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002648 if not title:
2649 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2650 title = ask_for_data(
2651 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002652 if title == default_title:
2653 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002654 change_id = self._GetChangeDetail()['change_id']
2655 while True:
2656 footer_change_ids = git_footers.get_footer_change_id(message)
2657 if footer_change_ids == [change_id]:
2658 break
2659 if not footer_change_ids:
2660 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002661 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002662 continue
2663 # There is already a valid footer but with different or several ids.
2664 # Doing this automatically is non-trivial as we don't want to lose
2665 # existing other footers, yet we want to append just 1 desired
2666 # Change-Id. Thus, just create a new footer, but let user verify the
2667 # new description.
2668 message = '%s\n\nChange-Id: %s' % (message, change_id)
2669 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002670 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002671 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002672 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002673 'Please, check the proposed correction to the description, '
2674 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2675 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2676 change_id))
2677 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2678 if not options.force:
2679 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002680 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002681 message = change_desc.description
2682 if not message:
2683 DieWithError("Description is empty. Aborting...")
2684 # Continue the while loop.
2685 # Sanity check of this code - we should end up with proper message
2686 # footer.
2687 assert [change_id] == git_footers.get_footer_change_id(message)
2688 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002689 else: # if not self.GetIssue()
2690 if options.message:
2691 message = options.message
2692 else:
2693 message = CreateDescriptionFromLog(args)
2694 if options.title:
2695 message = options.title + '\n\n' + message
2696 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002697 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002698 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002699 # On first upload, patchset title is always this string, while
2700 # --title flag gets converted to first line of message.
2701 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002702 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002703 if not change_desc.description:
2704 DieWithError("Description is empty. Aborting...")
2705 message = change_desc.description
2706 change_ids = git_footers.get_footer_change_id(message)
2707 if len(change_ids) > 1:
2708 DieWithError('too many Change-Id footers, at most 1 allowed.')
2709 if not change_ids:
2710 # Generate the Change-Id automatically.
2711 message = git_footers.add_footer_change_id(
2712 message, GenerateGerritChangeId(message))
2713 change_desc.set_description(message)
2714 change_ids = git_footers.get_footer_change_id(message)
2715 assert len(change_ids) == 1
2716 change_id = change_ids[0]
2717
2718 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2719 if remote is '.':
2720 # If our upstream branch is local, we base our squashed commit on its
2721 # squashed version.
2722 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2723 # Check the squashed hash of the parent.
2724 parent = RunGit(['config',
2725 'branch.%s.gerritsquashhash' % upstream_branch_name],
2726 error_ok=True).strip()
2727 # Verify that the upstream branch has been uploaded too, otherwise
2728 # Gerrit will create additional CLs when uploading.
2729 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2730 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002731 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002732 '\nUpload upstream branch %s first.\n'
2733 'It is likely that this branch has been rebased since its last '
2734 'upload, so you just need to upload it again.\n'
2735 '(If you uploaded it with --no-squash, then branch dependencies '
2736 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002737 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002738 else:
2739 parent = self.GetCommonAncestorWithUpstream()
2740
2741 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2742 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2743 '-m', message]).strip()
2744 else:
2745 change_desc = ChangeDescription(
2746 options.message or CreateDescriptionFromLog(args))
2747 if not change_desc.description:
2748 DieWithError("Description is empty. Aborting...")
2749
2750 if not git_footers.get_footer_change_id(change_desc.description):
2751 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002752 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2753 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002754 ref_to_push = 'HEAD'
2755 parent = '%s/%s' % (gerrit_remote, branch)
2756 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2757
2758 assert change_desc
2759 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2760 ref_to_push)]).splitlines()
2761 if len(commits) > 1:
2762 print('WARNING: This will upload %d commits. Run the following command '
2763 'to see which commits will be uploaded: ' % len(commits))
2764 print('git log %s..%s' % (parent, ref_to_push))
2765 print('You can also use `git squash-branch` to squash these into a '
2766 'single commit.')
2767 ask_for_data('About to upload; enter to confirm.')
2768
2769 if options.reviewers or options.tbr_owners:
2770 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2771 change)
2772
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002773 # Extra options that can be specified at push time. Doc:
2774 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2775 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002776 if change_desc.get_reviewers(tbr_only=True):
2777 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2778 refspec_opts.append('l=Code-Review+1')
2779
Aaron Gable9b713dd2016-12-14 16:04:21 -08002780 if title:
2781 if not re.match(r'^[\w ]+$', title):
2782 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002783 if not automatic_title:
2784 print('WARNING: Patchset title may only contain alphanumeric chars '
2785 'and spaces. Cleaned up title:\n%s' % title)
2786 if not options.force:
2787 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002788 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2789 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002790 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002791
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002792 if options.send_mail:
2793 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002794 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002795 refspec_opts.append('notify=ALL')
2796 else:
2797 refspec_opts.append('notify=NONE')
2798
tandrii99a72f22016-08-17 14:33:24 -07002799 reviewers = change_desc.get_reviewers()
2800 if reviewers:
2801 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002802
agablec6787972016-09-09 16:13:34 -07002803 if options.private:
2804 refspec_opts.append('draft')
2805
rmistry9eadede2016-09-19 11:22:43 -07002806 if options.topic:
2807 # Documentation on Gerrit topics is here:
2808 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2809 refspec_opts.append('topic=%s' % options.topic)
2810
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002811 refspec_suffix = ''
2812 if refspec_opts:
2813 refspec_suffix = '%' + ','.join(refspec_opts)
2814 assert ' ' not in refspec_suffix, (
2815 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002816 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002817
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002818 try:
2819 push_stdout = gclient_utils.CheckCallAndFilter(
2820 ['git', 'push', gerrit_remote, refspec],
2821 print_stdout=True,
2822 # Flush after every line: useful for seeing progress when running as
2823 # recipe.
2824 filter_fn=lambda _: sys.stdout.flush())
2825 except subprocess2.CalledProcessError:
2826 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002827 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002828
2829 if options.squash:
2830 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2831 change_numbers = [m.group(1)
2832 for m in map(regex.match, push_stdout.splitlines())
2833 if m]
2834 if len(change_numbers) != 1:
2835 DieWithError(
2836 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002837 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002838 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002839 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002840
2841 # Add cc's from the CC_LIST and --cc flag (if any).
2842 cc = self.GetCCList().split(',')
2843 if options.cc:
2844 cc.extend(options.cc)
2845 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002846 if change_desc.get_cced():
2847 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002848 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002849 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002850 self._GetGerritHost(), self.GetIssue(), cc,
2851 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002852 return 0
2853
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002854 def _AddChangeIdToCommitMessage(self, options, args):
2855 """Re-commits using the current message, assumes the commit hook is in
2856 place.
2857 """
2858 log_desc = options.message or CreateDescriptionFromLog(args)
2859 git_command = ['commit', '--amend', '-m', log_desc]
2860 RunGit(git_command)
2861 new_log_desc = CreateDescriptionFromLog(args)
2862 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002863 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002864 return new_log_desc
2865 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002866 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002867
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002868 def SetCQState(self, new_state):
2869 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002870 vote_map = {
2871 _CQState.NONE: 0,
2872 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002873 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002874 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002875 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2876 if new_state == _CQState.DRY_RUN:
2877 # Don't spam everybody reviewer/owner.
2878 kwargs['notify'] = 'NONE'
2879 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002880
tandriie113dfd2016-10-11 10:20:12 -07002881 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002882 try:
2883 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002884 except GerritChangeNotExists:
2885 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002886
2887 if data['status'] in ('ABANDONED', 'MERGED'):
2888 return 'CL %s is closed' % self.GetIssue()
2889
2890 def GetTryjobProperties(self, patchset=None):
2891 """Returns dictionary of properties to launch tryjob."""
2892 data = self._GetChangeDetail(['ALL_REVISIONS'])
2893 patchset = int(patchset or self.GetPatchset())
2894 assert patchset
2895 revision_data = None # Pylint wants it to be defined.
2896 for revision_data in data['revisions'].itervalues():
2897 if int(revision_data['_number']) == patchset:
2898 break
2899 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002900 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002901 (patchset, self.GetIssue()))
2902 return {
2903 'patch_issue': self.GetIssue(),
2904 'patch_set': patchset or self.GetPatchset(),
2905 'patch_project': data['project'],
2906 'patch_storage': 'gerrit',
2907 'patch_ref': revision_data['fetch']['http']['ref'],
2908 'patch_repository_url': revision_data['fetch']['http']['url'],
2909 'patch_gerrit_url': self.GetCodereviewServer(),
2910 }
tandriie113dfd2016-10-11 10:20:12 -07002911
tandriide281ae2016-10-12 06:02:30 -07002912 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002913 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002914
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002915
2916_CODEREVIEW_IMPLEMENTATIONS = {
2917 'rietveld': _RietveldChangelistImpl,
2918 'gerrit': _GerritChangelistImpl,
2919}
2920
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002921
iannuccie53c9352016-08-17 14:40:40 -07002922def _add_codereview_issue_select_options(parser, extra=""):
2923 _add_codereview_select_options(parser)
2924
2925 text = ('Operate on this issue number instead of the current branch\'s '
2926 'implicit issue.')
2927 if extra:
2928 text += ' '+extra
2929 parser.add_option('-i', '--issue', type=int, help=text)
2930
2931
2932def _process_codereview_issue_select_options(parser, options):
2933 _process_codereview_select_options(parser, options)
2934 if options.issue is not None and not options.forced_codereview:
2935 parser.error('--issue must be specified with either --rietveld or --gerrit')
2936
2937
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002938def _add_codereview_select_options(parser):
2939 """Appends --gerrit and --rietveld options to force specific codereview."""
2940 parser.codereview_group = optparse.OptionGroup(
2941 parser, 'EXPERIMENTAL! Codereview override options')
2942 parser.add_option_group(parser.codereview_group)
2943 parser.codereview_group.add_option(
2944 '--gerrit', action='store_true',
2945 help='Force the use of Gerrit for codereview')
2946 parser.codereview_group.add_option(
2947 '--rietveld', action='store_true',
2948 help='Force the use of Rietveld for codereview')
2949
2950
2951def _process_codereview_select_options(parser, options):
2952 if options.gerrit and options.rietveld:
2953 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2954 options.forced_codereview = None
2955 if options.gerrit:
2956 options.forced_codereview = 'gerrit'
2957 elif options.rietveld:
2958 options.forced_codereview = 'rietveld'
2959
2960
tandriif9aefb72016-07-01 09:06:51 -07002961def _get_bug_line_values(default_project, bugs):
2962 """Given default_project and comma separated list of bugs, yields bug line
2963 values.
2964
2965 Each bug can be either:
2966 * a number, which is combined with default_project
2967 * string, which is left as is.
2968
2969 This function may produce more than one line, because bugdroid expects one
2970 project per line.
2971
2972 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2973 ['v8:123', 'chromium:789']
2974 """
2975 default_bugs = []
2976 others = []
2977 for bug in bugs.split(','):
2978 bug = bug.strip()
2979 if bug:
2980 try:
2981 default_bugs.append(int(bug))
2982 except ValueError:
2983 others.append(bug)
2984
2985 if default_bugs:
2986 default_bugs = ','.join(map(str, default_bugs))
2987 if default_project:
2988 yield '%s:%s' % (default_project, default_bugs)
2989 else:
2990 yield default_bugs
2991 for other in sorted(others):
2992 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2993 yield other
2994
2995
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002996class ChangeDescription(object):
2997 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002998 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002999 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003000 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003001 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003002
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003003 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003004 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003005
agable@chromium.org42c20792013-09-12 17:34:49 +00003006 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003007 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003008 return '\n'.join(self._description_lines)
3009
3010 def set_description(self, desc):
3011 if isinstance(desc, basestring):
3012 lines = desc.splitlines()
3013 else:
3014 lines = [line.rstrip() for line in desc]
3015 while lines and not lines[0]:
3016 lines.pop(0)
3017 while lines and not lines[-1]:
3018 lines.pop(-1)
3019 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003020
piman@chromium.org336f9122014-09-04 02:16:55 +00003021 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003022 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003023 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003024 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003025 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003026 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003027
agable@chromium.org42c20792013-09-12 17:34:49 +00003028 # Get the set of R= and TBR= lines and remove them from the desciption.
3029 regexp = re.compile(self.R_LINE)
3030 matches = [regexp.match(line) for line in self._description_lines]
3031 new_desc = [l for i, l in enumerate(self._description_lines)
3032 if not matches[i]]
3033 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003034
agable@chromium.org42c20792013-09-12 17:34:49 +00003035 # Construct new unified R= and TBR= lines.
3036 r_names = []
3037 tbr_names = []
3038 for match in matches:
3039 if not match:
3040 continue
3041 people = cleanup_list([match.group(2).strip()])
3042 if match.group(1) == 'TBR':
3043 tbr_names.extend(people)
3044 else:
3045 r_names.extend(people)
3046 for name in r_names:
3047 if name not in reviewers:
3048 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003049 if add_owners_tbr:
3050 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003051 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003052 all_reviewers = set(tbr_names + reviewers)
3053 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3054 all_reviewers)
3055 tbr_names.extend(owners_db.reviewers_for(missing_files,
3056 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003057 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3058 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3059
3060 # Put the new lines in the description where the old first R= line was.
3061 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3062 if 0 <= line_loc < len(self._description_lines):
3063 if new_tbr_line:
3064 self._description_lines.insert(line_loc, new_tbr_line)
3065 if new_r_line:
3066 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003067 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003068 if new_r_line:
3069 self.append_footer(new_r_line)
3070 if new_tbr_line:
3071 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003072
tandriif9aefb72016-07-01 09:06:51 -07003073 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003074 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003075 self.set_description([
3076 '# Enter a description of the change.',
3077 '# This will be displayed on the codereview site.',
3078 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003079 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003080 '--------------------',
3081 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003082
agable@chromium.org42c20792013-09-12 17:34:49 +00003083 regexp = re.compile(self.BUG_LINE)
3084 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003085 prefix = settings.GetBugPrefix()
3086 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3087 for value in values:
3088 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3089 self.append_footer('BUG=%s' % value)
3090
agable@chromium.org42c20792013-09-12 17:34:49 +00003091 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003092 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003093 if not content:
3094 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003095 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003096
3097 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003098 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3099 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003100 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003101 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003102
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003103 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003104 """Adds a footer line to the description.
3105
3106 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3107 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3108 that Gerrit footers are always at the end.
3109 """
3110 parsed_footer_line = git_footers.parse_footer(line)
3111 if parsed_footer_line:
3112 # Line is a gerrit footer in the form: Footer-Key: any value.
3113 # Thus, must be appended observing Gerrit footer rules.
3114 self.set_description(
3115 git_footers.add_footer(self.description,
3116 key=parsed_footer_line[0],
3117 value=parsed_footer_line[1]))
3118 return
3119
3120 if not self._description_lines:
3121 self._description_lines.append(line)
3122 return
3123
3124 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3125 if gerrit_footers:
3126 # git_footers.split_footers ensures that there is an empty line before
3127 # actual (gerrit) footers, if any. We have to keep it that way.
3128 assert top_lines and top_lines[-1] == ''
3129 top_lines, separator = top_lines[:-1], top_lines[-1:]
3130 else:
3131 separator = [] # No need for separator if there are no gerrit_footers.
3132
3133 prev_line = top_lines[-1] if top_lines else ''
3134 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3135 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3136 top_lines.append('')
3137 top_lines.append(line)
3138 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003139
tandrii99a72f22016-08-17 14:33:24 -07003140 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003141 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003142 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003143 reviewers = [match.group(2).strip()
3144 for match in matches
3145 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003146 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003147
bradnelsond975b302016-10-23 12:20:23 -07003148 def get_cced(self):
3149 """Retrieves the list of reviewers."""
3150 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3151 cced = [match.group(2).strip() for match in matches if match]
3152 return cleanup_list(cced)
3153
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003154 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3155 """Updates this commit description given the parent.
3156
3157 This is essentially what Gnumbd used to do.
3158 Consult https://goo.gl/WMmpDe for more details.
3159 """
3160 assert parent_msg # No, orphan branch creation isn't supported.
3161 assert parent_hash
3162 assert dest_ref
3163 parent_footer_map = git_footers.parse_footers(parent_msg)
3164 # This will also happily parse svn-position, which GnumbD is no longer
3165 # supporting. While we'd generate correct footers, the verifier plugin
3166 # installed in Gerrit will block such commit (ie git push below will fail).
3167 parent_position = git_footers.get_position(parent_footer_map)
3168
3169 # Cherry-picks may have last line obscuring their prior footers,
3170 # from git_footers perspective. This is also what Gnumbd did.
3171 cp_line = None
3172 if (self._description_lines and
3173 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3174 cp_line = self._description_lines.pop()
3175
3176 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3177
3178 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3179 # user interference with actual footers we'd insert below.
3180 for i, (k, v) in enumerate(parsed_footers):
3181 if k.startswith('Cr-'):
3182 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3183
3184 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003185 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003186 if parent_position[0] == dest_ref:
3187 # Same branch as parent.
3188 number = int(parent_position[1]) + 1
3189 else:
3190 number = 1 # New branch, and extra lineage.
3191 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3192 int(parent_position[1])))
3193
3194 parsed_footers.append(('Cr-Commit-Position',
3195 '%s@{#%d}' % (dest_ref, number)))
3196 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3197
3198 self._description_lines = top_lines
3199 if cp_line:
3200 self._description_lines.append(cp_line)
3201 if self._description_lines[-1] != '':
3202 self._description_lines.append('') # Ensure footer separator.
3203 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3204
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003205
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003206def get_approving_reviewers(props):
3207 """Retrieves the reviewers that approved a CL from the issue properties with
3208 messages.
3209
3210 Note that the list may contain reviewers that are not committer, thus are not
3211 considered by the CQ.
3212 """
3213 return sorted(
3214 set(
3215 message['sender']
3216 for message in props['messages']
3217 if message['approval'] and message['sender'] in props['reviewers']
3218 )
3219 )
3220
3221
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003222def FindCodereviewSettingsFile(filename='codereview.settings'):
3223 """Finds the given file starting in the cwd and going up.
3224
3225 Only looks up to the top of the repository unless an
3226 'inherit-review-settings-ok' file exists in the root of the repository.
3227 """
3228 inherit_ok_file = 'inherit-review-settings-ok'
3229 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003230 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003231 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3232 root = '/'
3233 while True:
3234 if filename in os.listdir(cwd):
3235 if os.path.isfile(os.path.join(cwd, filename)):
3236 return open(os.path.join(cwd, filename))
3237 if cwd == root:
3238 break
3239 cwd = os.path.dirname(cwd)
3240
3241
3242def LoadCodereviewSettingsFromFile(fileobj):
3243 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003244 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003245
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003246 def SetProperty(name, setting, unset_error_ok=False):
3247 fullname = 'rietveld.' + name
3248 if setting in keyvals:
3249 RunGit(['config', fullname, keyvals[setting]])
3250 else:
3251 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3252
tandrii48df5812016-10-17 03:55:37 -07003253 if not keyvals.get('GERRIT_HOST', False):
3254 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003255 # Only server setting is required. Other settings can be absent.
3256 # In that case, we ignore errors raised during option deletion attempt.
3257 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003258 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003259 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3260 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003261 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003262 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3263 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003264 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003265 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3266 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003267
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003268 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003269 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003270
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003271 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003272 RunGit(['config', 'gerrit.squash-uploads',
3273 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003274
tandrii@chromium.org28253532016-04-14 13:46:56 +00003275 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003276 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003277 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3278
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003279 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003280 # should be of the form
3281 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3282 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003283 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3284 keyvals['ORIGIN_URL_CONFIG']])
3285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003286
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003287def urlretrieve(source, destination):
3288 """urllib is broken for SSL connections via a proxy therefore we
3289 can't use urllib.urlretrieve()."""
3290 with open(destination, 'w') as f:
3291 f.write(urllib2.urlopen(source).read())
3292
3293
ukai@chromium.org712d6102013-11-27 00:52:58 +00003294def hasSheBang(fname):
3295 """Checks fname is a #! script."""
3296 with open(fname) as f:
3297 return f.read(2).startswith('#!')
3298
3299
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003300# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3301def DownloadHooks(*args, **kwargs):
3302 pass
3303
3304
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003305def DownloadGerritHook(force):
3306 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003307
3308 Args:
3309 force: True to update hooks. False to install hooks if not present.
3310 """
3311 if not settings.GetIsGerrit():
3312 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003313 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003314 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3315 if not os.access(dst, os.X_OK):
3316 if os.path.exists(dst):
3317 if not force:
3318 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003319 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003320 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003321 if not hasSheBang(dst):
3322 DieWithError('Not a script: %s\n'
3323 'You need to download from\n%s\n'
3324 'into .git/hooks/commit-msg and '
3325 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003326 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3327 except Exception:
3328 if os.path.exists(dst):
3329 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003330 DieWithError('\nFailed to download hooks.\n'
3331 'You need to download from\n%s\n'
3332 'into .git/hooks/commit-msg and '
3333 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003334
3335
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003336def GetRietveldCodereviewSettingsInteractively():
3337 """Prompt the user for settings."""
3338 server = settings.GetDefaultServerUrl(error_ok=True)
3339 prompt = 'Rietveld server (host[:port])'
3340 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3341 newserver = ask_for_data(prompt + ':')
3342 if not server and not newserver:
3343 newserver = DEFAULT_SERVER
3344 if newserver:
3345 newserver = gclient_utils.UpgradeToHttps(newserver)
3346 if newserver != server:
3347 RunGit(['config', 'rietveld.server', newserver])
3348
3349 def SetProperty(initial, caption, name, is_url):
3350 prompt = caption
3351 if initial:
3352 prompt += ' ("x" to clear) [%s]' % initial
3353 new_val = ask_for_data(prompt + ':')
3354 if new_val == 'x':
3355 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3356 elif new_val:
3357 if is_url:
3358 new_val = gclient_utils.UpgradeToHttps(new_val)
3359 if new_val != initial:
3360 RunGit(['config', 'rietveld.' + name, new_val])
3361
3362 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3363 SetProperty(settings.GetDefaultPrivateFlag(),
3364 'Private flag (rietveld only)', 'private', False)
3365 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3366 'tree-status-url', False)
3367 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3368 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3369 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3370 'run-post-upload-hook', False)
3371
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003372
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003373@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003374def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003375 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003376
tandrii5d0a0422016-09-14 06:24:35 -07003377 print('WARNING: git cl config works for Rietveld only')
3378 # TODO(tandrii): remove this once we switch to Gerrit.
3379 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003380 parser.add_option('--activate-update', action='store_true',
3381 help='activate auto-updating [rietveld] section in '
3382 '.git/config')
3383 parser.add_option('--deactivate-update', action='store_true',
3384 help='deactivate auto-updating [rietveld] section in '
3385 '.git/config')
3386 options, args = parser.parse_args(args)
3387
3388 if options.deactivate_update:
3389 RunGit(['config', 'rietveld.autoupdate', 'false'])
3390 return
3391
3392 if options.activate_update:
3393 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3394 return
3395
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003396 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003397 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003398 return 0
3399
3400 url = args[0]
3401 if not url.endswith('codereview.settings'):
3402 url = os.path.join(url, 'codereview.settings')
3403
3404 # Load code review settings and download hooks (if available).
3405 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3406 return 0
3407
3408
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003409def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003410 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003411 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3412 branch = ShortBranchName(branchref)
3413 _, args = parser.parse_args(args)
3414 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003415 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003416 return RunGit(['config', 'branch.%s.base-url' % branch],
3417 error_ok=False).strip()
3418 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003419 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003420 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3421 error_ok=False).strip()
3422
3423
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003424def color_for_status(status):
3425 """Maps a Changelist status to color, for CMDstatus and other tools."""
3426 return {
3427 'unsent': Fore.RED,
3428 'waiting': Fore.BLUE,
3429 'reply': Fore.YELLOW,
3430 'lgtm': Fore.GREEN,
3431 'commit': Fore.MAGENTA,
3432 'closed': Fore.CYAN,
3433 'error': Fore.WHITE,
3434 }.get(status, Fore.WHITE)
3435
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003436
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003437def get_cl_statuses(changes, fine_grained, max_processes=None):
3438 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003439
3440 If fine_grained is true, this will fetch CL statuses from the server.
3441 Otherwise, simply indicate if there's a matching url for the given branches.
3442
3443 If max_processes is specified, it is used as the maximum number of processes
3444 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3445 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003446
3447 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003448 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003449 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003450 upload.verbosity = 0
3451
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003452 if not changes:
3453 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003454
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003455 if not fine_grained:
3456 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003457 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003458 for cl in changes:
3459 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003460 return
3461
3462 # First, sort out authentication issues.
3463 logging.debug('ensuring credentials exist')
3464 for cl in changes:
3465 cl.EnsureAuthenticated(force=False, refresh=True)
3466
3467 def fetch(cl):
3468 try:
3469 return (cl, cl.GetStatus())
3470 except:
3471 # See http://crbug.com/629863.
3472 logging.exception('failed to fetch status for %s:', cl)
3473 raise
3474
3475 threads_count = len(changes)
3476 if max_processes:
3477 threads_count = max(1, min(threads_count, max_processes))
3478 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3479
3480 pool = ThreadPool(threads_count)
3481 fetched_cls = set()
3482 try:
3483 it = pool.imap_unordered(fetch, changes).__iter__()
3484 while True:
3485 try:
3486 cl, status = it.next(timeout=5)
3487 except multiprocessing.TimeoutError:
3488 break
3489 fetched_cls.add(cl)
3490 yield cl, status
3491 finally:
3492 pool.close()
3493
3494 # Add any branches that failed to fetch.
3495 for cl in set(changes) - fetched_cls:
3496 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003497
rmistry@google.com2dd99862015-06-22 12:22:18 +00003498
3499def upload_branch_deps(cl, args):
3500 """Uploads CLs of local branches that are dependents of the current branch.
3501
3502 If the local branch dependency tree looks like:
3503 test1 -> test2.1 -> test3.1
3504 -> test3.2
3505 -> test2.2 -> test3.3
3506
3507 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3508 run on the dependent branches in this order:
3509 test2.1, test3.1, test3.2, test2.2, test3.3
3510
3511 Note: This function does not rebase your local dependent branches. Use it when
3512 you make a change to the parent branch that will not conflict with its
3513 dependent branches, and you would like their dependencies updated in
3514 Rietveld.
3515 """
3516 if git_common.is_dirty_git_tree('upload-branch-deps'):
3517 return 1
3518
3519 root_branch = cl.GetBranch()
3520 if root_branch is None:
3521 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3522 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003523 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003524 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3525 'patchset dependencies without an uploaded CL.')
3526
3527 branches = RunGit(['for-each-ref',
3528 '--format=%(refname:short) %(upstream:short)',
3529 'refs/heads'])
3530 if not branches:
3531 print('No local branches found.')
3532 return 0
3533
3534 # Create a dictionary of all local branches to the branches that are dependent
3535 # on it.
3536 tracked_to_dependents = collections.defaultdict(list)
3537 for b in branches.splitlines():
3538 tokens = b.split()
3539 if len(tokens) == 2:
3540 branch_name, tracked = tokens
3541 tracked_to_dependents[tracked].append(branch_name)
3542
vapiera7fbd5a2016-06-16 09:17:49 -07003543 print()
3544 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003545 dependents = []
3546 def traverse_dependents_preorder(branch, padding=''):
3547 dependents_to_process = tracked_to_dependents.get(branch, [])
3548 padding += ' '
3549 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003550 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003551 dependents.append(dependent)
3552 traverse_dependents_preorder(dependent, padding)
3553 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003554 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003555
3556 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003557 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003558 return 0
3559
vapiera7fbd5a2016-06-16 09:17:49 -07003560 print('This command will checkout all dependent branches and run '
3561 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003562 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3563
andybons@chromium.org962f9462016-02-03 20:00:42 +00003564 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003565 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003566 args.extend(['-t', 'Updated patchset dependency'])
3567
rmistry@google.com2dd99862015-06-22 12:22:18 +00003568 # Record all dependents that failed to upload.
3569 failures = {}
3570 # Go through all dependents, checkout the branch and upload.
3571 try:
3572 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003573 print()
3574 print('--------------------------------------')
3575 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003576 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003577 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003578 try:
3579 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003580 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003581 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003582 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003583 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003584 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003585 finally:
3586 # Swap back to the original root branch.
3587 RunGit(['checkout', '-q', root_branch])
3588
vapiera7fbd5a2016-06-16 09:17:49 -07003589 print()
3590 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003591 for dependent_branch in dependents:
3592 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003593 print(' %s : %s' % (dependent_branch, upload_status))
3594 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003595
3596 return 0
3597
3598
kmarshall3bff56b2016-06-06 18:31:47 -07003599def CMDarchive(parser, args):
3600 """Archives and deletes branches associated with closed changelists."""
3601 parser.add_option(
3602 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003603 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003604 parser.add_option(
3605 '-f', '--force', action='store_true',
3606 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003607 parser.add_option(
3608 '-d', '--dry-run', action='store_true',
3609 help='Skip the branch tagging and removal steps.')
3610 parser.add_option(
3611 '-t', '--notags', action='store_true',
3612 help='Do not tag archived branches. '
3613 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003614
3615 auth.add_auth_options(parser)
3616 options, args = parser.parse_args(args)
3617 if args:
3618 parser.error('Unsupported args: %s' % ' '.join(args))
3619 auth_config = auth.extract_auth_config_from_options(options)
3620
3621 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3622 if not branches:
3623 return 0
3624
vapiera7fbd5a2016-06-16 09:17:49 -07003625 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003626 changes = [Changelist(branchref=b, auth_config=auth_config)
3627 for b in branches.splitlines()]
3628 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3629 statuses = get_cl_statuses(changes,
3630 fine_grained=True,
3631 max_processes=options.maxjobs)
3632 proposal = [(cl.GetBranch(),
3633 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3634 for cl, status in statuses
3635 if status == 'closed']
3636 proposal.sort()
3637
3638 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003639 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003640 return 0
3641
3642 current_branch = GetCurrentBranch()
3643
vapiera7fbd5a2016-06-16 09:17:49 -07003644 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003645 if options.notags:
3646 for next_item in proposal:
3647 print(' ' + next_item[0])
3648 else:
3649 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3650 for next_item in proposal:
3651 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003652
kmarshall9249e012016-08-23 12:02:16 -07003653 # Quit now on precondition failure or if instructed by the user, either
3654 # via an interactive prompt or by command line flags.
3655 if options.dry_run:
3656 print('\nNo changes were made (dry run).\n')
3657 return 0
3658 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003659 print('You are currently on a branch \'%s\' which is associated with a '
3660 'closed codereview issue, so archive cannot proceed. Please '
3661 'checkout another branch and run this command again.' %
3662 current_branch)
3663 return 1
kmarshall9249e012016-08-23 12:02:16 -07003664 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003665 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3666 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003667 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003668 return 1
3669
3670 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003671 if not options.notags:
3672 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003673 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003674
vapiera7fbd5a2016-06-16 09:17:49 -07003675 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003676
3677 return 0
3678
3679
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003680def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003681 """Show status of changelists.
3682
3683 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003684 - Red not sent for review or broken
3685 - Blue waiting for review
3686 - Yellow waiting for you to reply to review
3687 - Green LGTM'ed
3688 - Magenta in the commit queue
3689 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003690
3691 Also see 'git cl comments'.
3692 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003693 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003694 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003695 parser.add_option('-f', '--fast', action='store_true',
3696 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003697 parser.add_option(
3698 '-j', '--maxjobs', action='store', type=int,
3699 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003700
3701 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003702 _add_codereview_issue_select_options(
3703 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003704 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003705 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003706 if args:
3707 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003708 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003709
iannuccie53c9352016-08-17 14:40:40 -07003710 if options.issue is not None and not options.field:
3711 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003712
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003713 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003714 cl = Changelist(auth_config=auth_config, issue=options.issue,
3715 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003716 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003717 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003718 elif options.field == 'id':
3719 issueid = cl.GetIssue()
3720 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003721 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003722 elif options.field == 'patch':
3723 patchset = cl.GetPatchset()
3724 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003725 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003726 elif options.field == 'status':
3727 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003728 elif options.field == 'url':
3729 url = cl.GetIssueURL()
3730 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003731 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003732 return 0
3733
3734 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3735 if not branches:
3736 print('No local branch found.')
3737 return 0
3738
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003739 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003740 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003741 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003742 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003743 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003744 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003745 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003746
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003747 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003748 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3749 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3750 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003751 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003752 c, status = output.next()
3753 branch_statuses[c.GetBranch()] = status
3754 status = branch_statuses.pop(branch)
3755 url = cl.GetIssueURL()
3756 if url and (not status or status == 'error'):
3757 # The issue probably doesn't exist anymore.
3758 url += ' (broken)'
3759
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003760 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003761 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003762 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003763 color = ''
3764 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003765 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003766 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003767 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003768 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003769
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003770
3771 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003772 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003773 print('Current branch: %s' % branch)
3774 for cl in changes:
3775 if cl.GetBranch() == branch:
3776 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003777 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003778 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003779 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003780 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003781 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003782 print('Issue description:')
3783 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003784 return 0
3785
3786
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003787def colorize_CMDstatus_doc():
3788 """To be called once in main() to add colors to git cl status help."""
3789 colors = [i for i in dir(Fore) if i[0].isupper()]
3790
3791 def colorize_line(line):
3792 for color in colors:
3793 if color in line.upper():
3794 # Extract whitespaces first and the leading '-'.
3795 indent = len(line) - len(line.lstrip(' ')) + 1
3796 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3797 return line
3798
3799 lines = CMDstatus.__doc__.splitlines()
3800 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3801
3802
phajdan.jre328cf92016-08-22 04:12:17 -07003803def write_json(path, contents):
3804 with open(path, 'w') as f:
3805 json.dump(contents, f)
3806
3807
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003808@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003809def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003810 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003811
3812 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003813 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003814 parser.add_option('-r', '--reverse', action='store_true',
3815 help='Lookup the branch(es) for the specified issues. If '
3816 'no issues are specified, all branches with mapped '
3817 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003818 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003819 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003820 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003821 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003822
dnj@chromium.org406c4402015-03-03 17:22:28 +00003823 if options.reverse:
3824 branches = RunGit(['for-each-ref', 'refs/heads',
3825 '--format=%(refname:short)']).splitlines()
3826
3827 # Reverse issue lookup.
3828 issue_branch_map = {}
3829 for branch in branches:
3830 cl = Changelist(branchref=branch)
3831 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3832 if not args:
3833 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003834 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003835 for issue in args:
3836 if not issue:
3837 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003838 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003839 print('Branch for issue number %s: %s' % (
3840 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003841 if options.json:
3842 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003843 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003844 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003845 if len(args) > 0:
3846 try:
3847 issue = int(args[0])
3848 except ValueError:
3849 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003850 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003851 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003852 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003853 if options.json:
3854 write_json(options.json, {
3855 'issue': cl.GetIssue(),
3856 'issue_url': cl.GetIssueURL(),
3857 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003858 return 0
3859
3860
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003861def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003862 """Shows or posts review comments for any changelist."""
3863 parser.add_option('-a', '--add-comment', dest='comment',
3864 help='comment to add to an issue')
3865 parser.add_option('-i', dest='issue',
3866 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003867 parser.add_option('-j', '--json-file',
3868 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003869 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003870 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003871 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003872
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003873 issue = None
3874 if options.issue:
3875 try:
3876 issue = int(options.issue)
3877 except ValueError:
3878 DieWithError('A review issue id is expected to be a number')
3879
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003880 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003881
3882 if options.comment:
3883 cl.AddComment(options.comment)
3884 return 0
3885
3886 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003887 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003888 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003889 summary.append({
3890 'date': message['date'],
3891 'lgtm': False,
3892 'message': message['text'],
3893 'not_lgtm': False,
3894 'sender': message['sender'],
3895 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003896 if message['disapproval']:
3897 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003898 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003899 elif message['approval']:
3900 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003901 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003902 elif message['sender'] == data['owner_email']:
3903 color = Fore.MAGENTA
3904 else:
3905 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003906 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003907 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003908 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003909 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003910 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003911 if options.json_file:
3912 with open(options.json_file, 'wb') as f:
3913 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003914 return 0
3915
3916
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003917@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003918def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003919 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003920 parser.add_option('-d', '--display', action='store_true',
3921 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003922 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003923 help='New description to set for this issue (- for stdin, '
3924 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003925 parser.add_option('-f', '--force', action='store_true',
3926 help='Delete any unpublished Gerrit edits for this issue '
3927 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003928
3929 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003930 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003931 options, args = parser.parse_args(args)
3932 _process_codereview_select_options(parser, options)
3933
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003934 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003935 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003936 target_issue_arg = ParseIssueNumberArgument(args[0])
3937 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003938 parser.print_help()
3939 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003940
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003941 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003942
martiniss6eda05f2016-06-30 10:18:35 -07003943 kwargs = {
3944 'auth_config': auth_config,
3945 'codereview': options.forced_codereview,
3946 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003947 if target_issue_arg:
3948 kwargs['issue'] = target_issue_arg.issue
3949 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07003950
3951 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003952
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003953 if not cl.GetIssue():
3954 DieWithError('This branch has no associated changelist.')
3955 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003956
smut@google.com34fb6b12015-07-13 20:03:26 +00003957 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003958 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003959 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003960
3961 if options.new_description:
3962 text = options.new_description
3963 if text == '-':
3964 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003965 elif text == '+':
3966 base_branch = cl.GetCommonAncestorWithUpstream()
3967 change = cl.GetChange(base_branch, None, local_description=True)
3968 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003969
3970 description.set_description(text)
3971 else:
3972 description.prompt()
3973
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003974 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003975 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003976 return 0
3977
3978
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003979def CreateDescriptionFromLog(args):
3980 """Pulls out the commit log to use as a base for the CL description."""
3981 log_args = []
3982 if len(args) == 1 and not args[0].endswith('.'):
3983 log_args = [args[0] + '..']
3984 elif len(args) == 1 and args[0].endswith('...'):
3985 log_args = [args[0][:-1]]
3986 elif len(args) == 2:
3987 log_args = [args[0] + '..' + args[1]]
3988 else:
3989 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003990 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003991
3992
thestig@chromium.org44202a22014-03-11 19:22:18 +00003993def CMDlint(parser, args):
3994 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003995 parser.add_option('--filter', action='append', metavar='-x,+y',
3996 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003997 auth.add_auth_options(parser)
3998 options, args = parser.parse_args(args)
3999 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004000
4001 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004002 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004003 try:
4004 import cpplint
4005 import cpplint_chromium
4006 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004007 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004008 return 1
4009
4010 # Change the current working directory before calling lint so that it
4011 # shows the correct base.
4012 previous_cwd = os.getcwd()
4013 os.chdir(settings.GetRoot())
4014 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004015 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004016 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4017 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004018 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004019 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004020 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004021
4022 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004023 command = args + files
4024 if options.filter:
4025 command = ['--filter=' + ','.join(options.filter)] + command
4026 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004027
4028 white_regex = re.compile(settings.GetLintRegex())
4029 black_regex = re.compile(settings.GetLintIgnoreRegex())
4030 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4031 for filename in filenames:
4032 if white_regex.match(filename):
4033 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004034 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004035 else:
4036 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4037 extra_check_functions)
4038 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004039 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004040 finally:
4041 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004042 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004043 if cpplint._cpplint_state.error_count != 0:
4044 return 1
4045 return 0
4046
4047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004048def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004049 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004050 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004051 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004052 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004053 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004054 auth.add_auth_options(parser)
4055 options, args = parser.parse_args(args)
4056 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004057
sbc@chromium.org71437c02015-04-09 19:29:40 +00004058 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004059 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004060 return 1
4061
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004062 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063 if args:
4064 base_branch = args[0]
4065 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004066 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004067 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004068
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004069 cl.RunHook(
4070 committing=not options.upload,
4071 may_prompt=False,
4072 verbose=options.verbose,
4073 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004074 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004075
4076
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004077def GenerateGerritChangeId(message):
4078 """Returns Ixxxxxx...xxx change id.
4079
4080 Works the same way as
4081 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4082 but can be called on demand on all platforms.
4083
4084 The basic idea is to generate git hash of a state of the tree, original commit
4085 message, author/committer info and timestamps.
4086 """
4087 lines = []
4088 tree_hash = RunGitSilent(['write-tree'])
4089 lines.append('tree %s' % tree_hash.strip())
4090 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4091 if code == 0:
4092 lines.append('parent %s' % parent.strip())
4093 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4094 lines.append('author %s' % author.strip())
4095 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4096 lines.append('committer %s' % committer.strip())
4097 lines.append('')
4098 # Note: Gerrit's commit-hook actually cleans message of some lines and
4099 # whitespace. This code is not doing this, but it clearly won't decrease
4100 # entropy.
4101 lines.append(message)
4102 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4103 stdin='\n'.join(lines))
4104 return 'I%s' % change_hash.strip()
4105
4106
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004107def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004108 """Computes the remote branch ref to use for the CL.
4109
4110 Args:
4111 remote (str): The git remote for the CL.
4112 remote_branch (str): The git remote branch for the CL.
4113 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004114 """
4115 if not (remote and remote_branch):
4116 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004117
wittman@chromium.org455dc922015-01-26 20:15:50 +00004118 if target_branch:
4119 # Cannonicalize branch references to the equivalent local full symbolic
4120 # refs, which are then translated into the remote full symbolic refs
4121 # below.
4122 if '/' not in target_branch:
4123 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4124 else:
4125 prefix_replacements = (
4126 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4127 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4128 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4129 )
4130 match = None
4131 for regex, replacement in prefix_replacements:
4132 match = re.search(regex, target_branch)
4133 if match:
4134 remote_branch = target_branch.replace(match.group(0), replacement)
4135 break
4136 if not match:
4137 # This is a branch path but not one we recognize; use as-is.
4138 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004139 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4140 # Handle the refs that need to land in different refs.
4141 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004142
wittman@chromium.org455dc922015-01-26 20:15:50 +00004143 # Create the true path to the remote branch.
4144 # Does the following translation:
4145 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4146 # * refs/remotes/origin/master -> refs/heads/master
4147 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4148 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4149 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4150 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4151 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4152 'refs/heads/')
4153 elif remote_branch.startswith('refs/remotes/branch-heads'):
4154 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004155
wittman@chromium.org455dc922015-01-26 20:15:50 +00004156 return remote_branch
4157
4158
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004159def cleanup_list(l):
4160 """Fixes a list so that comma separated items are put as individual items.
4161
4162 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4163 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4164 """
4165 items = sum((i.split(',') for i in l), [])
4166 stripped_items = (i.strip() for i in items)
4167 return sorted(filter(None, stripped_items))
4168
4169
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004170@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004171def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004172 """Uploads the current changelist to codereview.
4173
4174 Can skip dependency patchset uploads for a branch by running:
4175 git config branch.branch_name.skip-deps-uploads True
4176 To unset run:
4177 git config --unset branch.branch_name.skip-deps-uploads
4178 Can also set the above globally by using the --global flag.
4179 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004180 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4181 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004182 parser.add_option('--bypass-watchlists', action='store_true',
4183 dest='bypass_watchlists',
4184 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004185 parser.add_option('-f', action='store_true', dest='force',
4186 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004187 parser.add_option('--message', '-m', dest='message',
4188 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004189 parser.add_option('-b', '--bug',
4190 help='pre-populate the bug number(s) for this issue. '
4191 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004192 parser.add_option('--message-file', dest='message_file',
4193 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004194 parser.add_option('--title', '-t', dest='title',
4195 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004196 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004197 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004198 help='reviewer email addresses')
4199 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004200 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004201 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004202 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004203 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004204 parser.add_option('--emulate_svn_auto_props',
4205 '--emulate-svn-auto-props',
4206 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004207 dest="emulate_svn_auto_props",
4208 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004209 parser.add_option('-c', '--use-commit-queue', action='store_true',
4210 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004211 parser.add_option('--private', action='store_true',
4212 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004213 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004214 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004215 metavar='TARGET',
4216 help='Apply CL to remote ref TARGET. ' +
4217 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004218 parser.add_option('--squash', action='store_true',
4219 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004220 parser.add_option('--no-squash', action='store_true',
4221 help='Don\'t squash multiple commits into one ' +
4222 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004223 parser.add_option('--topic', default=None,
4224 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004225 parser.add_option('--email', default=None,
4226 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004227 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4228 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004229 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4230 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004231 help='Send the patchset to do a CQ dry run right after '
4232 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004233 parser.add_option('--dependencies', action='store_true',
4234 help='Uploads CLs of all the local branches that depend on '
4235 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004236
rmistry@google.com2dd99862015-06-22 12:22:18 +00004237 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004238 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004239 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004240 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004241 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004242 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004243 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004244
sbc@chromium.org71437c02015-04-09 19:29:40 +00004245 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004246 return 1
4247
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004248 options.reviewers = cleanup_list(options.reviewers)
4249 options.cc = cleanup_list(options.cc)
4250
tandriib80458a2016-06-23 12:20:07 -07004251 if options.message_file:
4252 if options.message:
4253 parser.error('only one of --message and --message-file allowed.')
4254 options.message = gclient_utils.FileRead(options.message_file)
4255 options.message_file = None
4256
tandrii4d0545a2016-07-06 03:56:49 -07004257 if options.cq_dry_run and options.use_commit_queue:
4258 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4259
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004260 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4261 settings.GetIsGerrit()
4262
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004263 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004264 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004265
4266
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004267@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004268def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004269 """DEPRECATED: Used to commit the current changelist via git-svn."""
4270 message = ('git-cl no longer supports committing to SVN repositories via '
4271 'git-svn. You probably want to use `git cl land` instead.')
4272 print(message)
4273 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004274
4275
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004276@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004277def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004278 """Commits the current changelist via git.
4279
4280 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4281 upstream and closes the issue automatically and atomically.
4282
4283 Otherwise (in case of Rietveld):
4284 Squashes branch into a single commit.
4285 Updates commit message with metadata (e.g. pointer to review).
4286 Pushes the code upstream.
4287 Updates review and closes.
4288 """
4289 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4290 help='bypass upload presubmit hook')
4291 parser.add_option('-m', dest='message',
4292 help="override review description")
4293 parser.add_option('-f', action='store_true', dest='force',
4294 help="force yes to questions (don't prompt)")
4295 parser.add_option('-c', dest='contributor',
4296 help="external contributor for patch (appended to " +
4297 "description and used as author for git). Should be " +
4298 "formatted as 'First Last <email@example.com>'")
4299 add_git_similarity(parser)
4300 auth.add_auth_options(parser)
4301 (options, args) = parser.parse_args(args)
4302 auth_config = auth.extract_auth_config_from_options(options)
4303
4304 cl = Changelist(auth_config=auth_config)
4305
4306 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4307 if cl.IsGerrit():
4308 if options.message:
4309 # This could be implemented, but it requires sending a new patch to
4310 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4311 # Besides, Gerrit has the ability to change the commit message on submit
4312 # automatically, thus there is no need to support this option (so far?).
4313 parser.error('-m MESSAGE option is not supported for Gerrit.')
4314 if options.contributor:
4315 parser.error(
4316 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4317 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4318 'the contributor\'s "name <email>". If you can\'t upload such a '
4319 'commit for review, contact your repository admin and request'
4320 '"Forge-Author" permission.')
4321 if not cl.GetIssue():
4322 DieWithError('You must upload the change first to Gerrit.\n'
4323 ' If you would rather have `git cl land` upload '
4324 'automatically for you, see http://crbug.com/642759')
4325 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4326 options.verbose)
4327
4328 current = cl.GetBranch()
4329 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4330 if remote == '.':
4331 print()
4332 print('Attempting to push branch %r into another local branch!' % current)
4333 print()
4334 print('Either reparent this branch on top of origin/master:')
4335 print(' git reparent-branch --root')
4336 print()
4337 print('OR run `git rebase-update` if you think the parent branch is ')
4338 print('already committed.')
4339 print()
4340 print(' Current parent: %r' % upstream_branch)
4341 return 1
4342
4343 if not args:
4344 # Default to merging against our best guess of the upstream branch.
4345 args = [cl.GetUpstreamBranch()]
4346
4347 if options.contributor:
4348 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4349 print("Please provide contibutor as 'First Last <email@example.com>'")
4350 return 1
4351
4352 base_branch = args[0]
4353
4354 if git_common.is_dirty_git_tree('land'):
4355 return 1
4356
4357 # This rev-list syntax means "show all commits not in my branch that
4358 # are in base_branch".
4359 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4360 base_branch]).splitlines()
4361 if upstream_commits:
4362 print('Base branch "%s" has %d commits '
4363 'not in this branch.' % (base_branch, len(upstream_commits)))
4364 print('Run "git merge %s" before attempting to land.' % base_branch)
4365 return 1
4366
4367 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4368 if not options.bypass_hooks:
4369 author = None
4370 if options.contributor:
4371 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4372 hook_results = cl.RunHook(
4373 committing=True,
4374 may_prompt=not options.force,
4375 verbose=options.verbose,
4376 change=cl.GetChange(merge_base, author))
4377 if not hook_results.should_continue():
4378 return 1
4379
4380 # Check the tree status if the tree status URL is set.
4381 status = GetTreeStatus()
4382 if 'closed' == status:
4383 print('The tree is closed. Please wait for it to reopen. Use '
4384 '"git cl land --bypass-hooks" to commit on a closed tree.')
4385 return 1
4386 elif 'unknown' == status:
4387 print('Unable to determine tree status. Please verify manually and '
4388 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4389 return 1
4390
4391 change_desc = ChangeDescription(options.message)
4392 if not change_desc.description and cl.GetIssue():
4393 change_desc = ChangeDescription(cl.GetDescription())
4394
4395 if not change_desc.description:
4396 if not cl.GetIssue() and options.bypass_hooks:
4397 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4398 else:
4399 print('No description set.')
4400 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4401 return 1
4402
4403 # Keep a separate copy for the commit message, because the commit message
4404 # contains the link to the Rietveld issue, while the Rietveld message contains
4405 # the commit viewvc url.
4406 if cl.GetIssue():
4407 change_desc.update_reviewers(cl.GetApprovingReviewers())
4408
4409 commit_desc = ChangeDescription(change_desc.description)
4410 if cl.GetIssue():
4411 # Xcode won't linkify this URL unless there is a non-whitespace character
4412 # after it. Add a period on a new line to circumvent this. Also add a space
4413 # before the period to make sure that Gitiles continues to correctly resolve
4414 # the URL.
4415 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4416 if options.contributor:
4417 commit_desc.append_footer('Patch from %s.' % options.contributor)
4418
4419 print('Description:')
4420 print(commit_desc.description)
4421
4422 branches = [merge_base, cl.GetBranchRef()]
4423 if not options.force:
4424 print_stats(options.similarity, options.find_copies, branches)
4425
4426 # We want to squash all this branch's commits into one commit with the proper
4427 # description. We do this by doing a "reset --soft" to the base branch (which
4428 # keeps the working copy the same), then landing that.
4429 MERGE_BRANCH = 'git-cl-commit'
4430 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4431 # Delete the branches if they exist.
4432 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4433 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4434 result = RunGitWithCode(showref_cmd)
4435 if result[0] == 0:
4436 RunGit(['branch', '-D', branch])
4437
4438 # We might be in a directory that's present in this branch but not in the
4439 # trunk. Move up to the top of the tree so that git commands that expect a
4440 # valid CWD won't fail after we check out the merge branch.
4441 rel_base_path = settings.GetRelativeRoot()
4442 if rel_base_path:
4443 os.chdir(rel_base_path)
4444
4445 # Stuff our change into the merge branch.
4446 # We wrap in a try...finally block so if anything goes wrong,
4447 # we clean up the branches.
4448 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004449 revision = None
4450 try:
4451 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4452 RunGit(['reset', '--soft', merge_base])
4453 if options.contributor:
4454 RunGit(
4455 [
4456 'commit', '--author', options.contributor,
4457 '-m', commit_desc.description,
4458 ])
4459 else:
4460 RunGit(['commit', '-m', commit_desc.description])
4461
4462 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4463 mirror = settings.GetGitMirror(remote)
4464 if mirror:
4465 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004466 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004467 else:
4468 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004469 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004470 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4471
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004472 if git_numberer_enabled:
4473 # TODO(tandrii): maybe do autorebase + retry on failure
4474 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004475 logging.debug('Adding git number footers')
4476 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4477 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4478 branch)
4479 # Ensure timestamps are monotonically increasing.
4480 timestamp = max(1 + _get_committer_timestamp(merge_base),
4481 _get_committer_timestamp('HEAD'))
4482 _git_amend_head(commit_desc.description, timestamp)
4483 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004484
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004485 retcode, output = RunGitWithCode(
4486 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004487 if retcode == 0:
4488 revision = RunGit(['rev-parse', 'HEAD']).strip()
4489 logging.debug(output)
4490 except: # pylint: disable=bare-except
4491 if _IS_BEING_TESTED:
4492 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4493 + '-' * 30 + '8<' + '-' * 30)
4494 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4495 raise
4496 finally:
4497 # And then swap back to the original branch and clean up.
4498 RunGit(['checkout', '-q', cl.GetBranch()])
4499 RunGit(['branch', '-D', MERGE_BRANCH])
4500
4501 if not revision:
4502 print('Failed to push. If this persists, please file a bug.')
4503 return 1
4504
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004505 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004506 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004507 if viewvc_url and revision:
4508 change_desc.append_footer(
4509 'Committed: %s%s' % (viewvc_url, revision))
4510 elif revision:
4511 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004512 print('Closing issue '
4513 '(you may be prompted for your codereview password)...')
4514 cl.UpdateDescription(change_desc.description)
4515 cl.CloseIssue()
4516 props = cl.GetIssueProperties()
4517 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004518 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4519 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004520 if options.bypass_hooks:
4521 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4522 else:
4523 comment += ' (presubmit successful).'
4524 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4525
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004526 if os.path.isfile(POSTUPSTREAM_HOOK):
4527 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4528
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004529 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004530
4531
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004532@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004533def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004534 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004535 parser.add_option('-b', dest='newbranch',
4536 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004537 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004538 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004539 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4540 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004541 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004542 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004543 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004544 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004545 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004546 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004547
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004548
4549 group = optparse.OptionGroup(
4550 parser,
4551 'Options for continuing work on the current issue uploaded from a '
4552 'different clone (e.g. different machine). Must be used independently '
4553 'from the other options. No issue number should be specified, and the '
4554 'branch must have an issue number associated with it')
4555 group.add_option('--reapply', action='store_true', dest='reapply',
4556 help='Reset the branch and reapply the issue.\n'
4557 'CAUTION: This will undo any local changes in this '
4558 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004559
4560 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004561 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004562 parser.add_option_group(group)
4563
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004564 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004565 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004566 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004567 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004568 auth_config = auth.extract_auth_config_from_options(options)
4569
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004570
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004571 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004572 if options.newbranch:
4573 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004574 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004575 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004576
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004577 cl = Changelist(auth_config=auth_config,
4578 codereview=options.forced_codereview)
4579 if not cl.GetIssue():
4580 parser.error('current branch must have an associated issue')
4581
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004582 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004583 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004584 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004585
4586 RunGit(['reset', '--hard', upstream])
4587 if options.pull:
4588 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004589
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004590 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4591 options.directory)
4592
4593 if len(args) != 1 or not args[0]:
4594 parser.error('Must specify issue number or url')
4595
4596 # We don't want uncommitted changes mixed up with the patch.
4597 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004598 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004599
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004600 if options.newbranch:
4601 if options.force:
4602 RunGit(['branch', '-D', options.newbranch],
4603 stderr=subprocess2.PIPE, error_ok=True)
4604 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004605 elif not GetCurrentBranch():
4606 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004607
4608 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4609
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004610 if cl.IsGerrit():
4611 if options.reject:
4612 parser.error('--reject is not supported with Gerrit codereview.')
4613 if options.nocommit:
4614 parser.error('--nocommit is not supported with Gerrit codereview.')
4615 if options.directory:
4616 parser.error('--directory is not supported with Gerrit codereview.')
4617
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004618 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004619 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004620
4621
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004622def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004623 """Fetches the tree status and returns either 'open', 'closed',
4624 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004625 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004626 if url:
4627 status = urllib2.urlopen(url).read().lower()
4628 if status.find('closed') != -1 or status == '0':
4629 return 'closed'
4630 elif status.find('open') != -1 or status == '1':
4631 return 'open'
4632 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004633 return 'unset'
4634
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004635
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004636def GetTreeStatusReason():
4637 """Fetches the tree status from a json url and returns the message
4638 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004639 url = settings.GetTreeStatusUrl()
4640 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004641 connection = urllib2.urlopen(json_url)
4642 status = json.loads(connection.read())
4643 connection.close()
4644 return status['message']
4645
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004647def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004648 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004649 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004650 status = GetTreeStatus()
4651 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004652 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004653 return 2
4654
vapiera7fbd5a2016-06-16 09:17:49 -07004655 print('The tree is %s' % status)
4656 print()
4657 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004658 if status != 'open':
4659 return 1
4660 return 0
4661
4662
maruel@chromium.org15192402012-09-06 12:38:29 +00004663def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004664 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004665 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004666 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004667 '-b', '--bot', action='append',
4668 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4669 'times to specify multiple builders. ex: '
4670 '"-b win_rel -b win_layout". See '
4671 'the try server waterfall for the builders name and the tests '
4672 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004673 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004674 '-B', '--bucket', default='',
4675 help=('Buildbucket bucket to send the try requests.'))
4676 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004677 '-m', '--master', default='',
4678 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004679 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004680 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004681 help='Revision to use for the try job; default: the revision will '
4682 'be determined by the try recipe that builder runs, which usually '
4683 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004684 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004685 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004686 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004687 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004688 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004689 '--project',
4690 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004691 'in recipe to determine to which repository or directory to '
4692 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004693 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004694 '-p', '--property', dest='properties', action='append', default=[],
4695 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004696 'key2=value2 etc. The value will be treated as '
4697 'json if decodable, or as string otherwise. '
4698 'NOTE: using this may make your try job not usable for CQ, '
4699 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004700 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004701 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4702 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004703 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004704 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004705 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004706 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004707
machenbach@chromium.org45453142015-09-15 08:45:22 +00004708 # Make sure that all properties are prop=value pairs.
4709 bad_params = [x for x in options.properties if '=' not in x]
4710 if bad_params:
4711 parser.error('Got properties with missing "=": %s' % bad_params)
4712
maruel@chromium.org15192402012-09-06 12:38:29 +00004713 if args:
4714 parser.error('Unknown arguments: %s' % args)
4715
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004716 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004717 if not cl.GetIssue():
4718 parser.error('Need to upload first')
4719
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004720 if cl.IsGerrit():
4721 # HACK: warm up Gerrit change detail cache to save on RPCs.
4722 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4723
tandriie113dfd2016-10-11 10:20:12 -07004724 error_message = cl.CannotTriggerTryJobReason()
4725 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004726 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004727
borenet6c0efe62016-10-19 08:13:29 -07004728 if options.bucket and options.master:
4729 parser.error('Only one of --bucket and --master may be used.')
4730
qyearsley1fdfcb62016-10-24 13:22:03 -07004731 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004732
qyearsleydd49f942016-10-28 11:57:22 -07004733 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4734 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004735 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004736 if options.verbose:
4737 print('git cl try with no bots now defaults to CQ Dry Run.')
4738 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004739
borenet6c0efe62016-10-19 08:13:29 -07004740 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004741 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004742 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004743 'of bot requires an initial job from a parent (usually a builder). '
4744 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004745 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004746 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004747
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004748 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004749 # TODO(tandrii): Checking local patchset against remote patchset is only
4750 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4751 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004752 print('Warning: Codereview server has newer patchsets (%s) than most '
4753 'recent upload from local checkout (%s). Did a previous upload '
4754 'fail?\n'
4755 'By default, git cl try uses the latest patchset from '
4756 'codereview, continuing to use patchset %s.\n' %
4757 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004758
tandrii568043b2016-10-11 07:49:18 -07004759 try:
borenet6c0efe62016-10-19 08:13:29 -07004760 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4761 patchset)
tandrii568043b2016-10-11 07:49:18 -07004762 except BuildbucketResponseException as ex:
4763 print('ERROR: %s' % ex)
4764 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004765 return 0
4766
4767
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004768def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004769 """Prints info about try jobs associated with current CL."""
4770 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004771 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004772 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004773 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004774 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004775 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004776 '--color', action='store_true', default=setup_color.IS_TTY,
4777 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004778 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004779 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4780 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004781 group.add_option(
4782 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004783 parser.add_option_group(group)
4784 auth.add_auth_options(parser)
4785 options, args = parser.parse_args(args)
4786 if args:
4787 parser.error('Unrecognized args: %s' % ' '.join(args))
4788
4789 auth_config = auth.extract_auth_config_from_options(options)
4790 cl = Changelist(auth_config=auth_config)
4791 if not cl.GetIssue():
4792 parser.error('Need to upload first')
4793
tandrii221ab252016-10-06 08:12:04 -07004794 patchset = options.patchset
4795 if not patchset:
4796 patchset = cl.GetMostRecentPatchset()
4797 if not patchset:
4798 parser.error('Codereview doesn\'t know about issue %s. '
4799 'No access to issue or wrong issue number?\n'
4800 'Either upload first, or pass --patchset explicitely' %
4801 cl.GetIssue())
4802
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004803 # TODO(tandrii): Checking local patchset against remote patchset is only
4804 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4805 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004806 print('Warning: Codereview server has newer patchsets (%s) than most '
4807 'recent upload from local checkout (%s). Did a previous upload '
4808 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004809 'By default, git cl try-results uses the latest patchset from '
4810 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004811 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004812 try:
tandrii221ab252016-10-06 08:12:04 -07004813 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004814 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004815 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004816 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004817 if options.json:
4818 write_try_results_json(options.json, jobs)
4819 else:
4820 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004821 return 0
4822
4823
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004824@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004825def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004826 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004827 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004828 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004829 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004830
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004831 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004832 if args:
4833 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004834 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004835 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004836 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004837 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004838
4839 # Clear configured merge-base, if there is one.
4840 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004841 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004842 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004843 return 0
4844
4845
thestig@chromium.org00858c82013-12-02 23:08:03 +00004846def CMDweb(parser, args):
4847 """Opens the current CL in the web browser."""
4848 _, args = parser.parse_args(args)
4849 if args:
4850 parser.error('Unrecognized args: %s' % ' '.join(args))
4851
4852 issue_url = Changelist().GetIssueURL()
4853 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004854 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004855 return 1
4856
4857 webbrowser.open(issue_url)
4858 return 0
4859
4860
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004861def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004862 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004863 parser.add_option('-d', '--dry-run', action='store_true',
4864 help='trigger in dry run mode')
4865 parser.add_option('-c', '--clear', action='store_true',
4866 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004867 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004868 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004869 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004870 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004871 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004872 if args:
4873 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004874 if options.dry_run and options.clear:
4875 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4876
iannuccie53c9352016-08-17 14:40:40 -07004877 cl = Changelist(auth_config=auth_config, issue=options.issue,
4878 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004879 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004880 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004881 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004882 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004883 state = _CQState.DRY_RUN
4884 else:
4885 state = _CQState.COMMIT
4886 if not cl.GetIssue():
4887 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004888 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004889 return 0
4890
4891
groby@chromium.org411034a2013-02-26 15:12:01 +00004892def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004893 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004894 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004895 auth.add_auth_options(parser)
4896 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004897 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004898 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004899 if args:
4900 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004901 cl = Changelist(auth_config=auth_config, issue=options.issue,
4902 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004903 # Ensure there actually is an issue to close.
4904 cl.GetDescription()
4905 cl.CloseIssue()
4906 return 0
4907
4908
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004909def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004910 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004911 parser.add_option(
4912 '--stat',
4913 action='store_true',
4914 dest='stat',
4915 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004916 auth.add_auth_options(parser)
4917 options, args = parser.parse_args(args)
4918 auth_config = auth.extract_auth_config_from_options(options)
4919 if args:
4920 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004921
4922 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004923 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004924 # Staged changes would be committed along with the patch from last
4925 # upload, hence counted toward the "last upload" side in the final
4926 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004927 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004928 return 1
4929
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004930 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004931 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004932 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004933 if not issue:
4934 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004935 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004936 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004937
4938 # Create a new branch based on the merge-base
4939 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004940 # Clear cached branch in cl object, to avoid overwriting original CL branch
4941 # properties.
4942 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004943 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004944 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004945 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004946 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004947 return rtn
4948
wychen@chromium.org06928532015-02-03 02:11:29 +00004949 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004950 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004951 cmd = ['git', 'diff']
4952 if options.stat:
4953 cmd.append('--stat')
4954 cmd.extend([TMP_BRANCH, branch, '--'])
4955 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004956 finally:
4957 RunGit(['checkout', '-q', branch])
4958 RunGit(['branch', '-D', TMP_BRANCH])
4959
4960 return 0
4961
4962
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004963def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004964 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004965 parser.add_option(
4966 '--no-color',
4967 action='store_true',
4968 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004969 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004970 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004971 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004972
4973 author = RunGit(['config', 'user.email']).strip() or None
4974
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004975 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004976
4977 if args:
4978 if len(args) > 1:
4979 parser.error('Unknown args')
4980 base_branch = args[0]
4981 else:
4982 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004983 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004984
4985 change = cl.GetChange(base_branch, None)
4986 return owners_finder.OwnersFinder(
4987 [f.LocalPath() for f in
4988 cl.GetChange(base_branch, None).AffectedFiles()],
4989 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004990 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004991 disable_color=options.no_color).run()
4992
4993
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004994def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004995 """Generates a diff command."""
4996 # Generate diff for the current branch's changes.
4997 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004998 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004999
5000 if args:
5001 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005002 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005003 diff_cmd.append(arg)
5004 else:
5005 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005006
5007 return diff_cmd
5008
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005009
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005010def MatchingFileType(file_name, extensions):
5011 """Returns true if the file name ends with one of the given extensions."""
5012 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005013
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005014
enne@chromium.org555cfe42014-01-29 18:21:39 +00005015@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005016def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005017 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005018 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005019 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005020 parser.add_option('--full', action='store_true',
5021 help='Reformat the full content of all touched files')
5022 parser.add_option('--dry-run', action='store_true',
5023 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005024 parser.add_option('--python', action='store_true',
5025 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005026 parser.add_option('--js', action='store_true',
5027 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005028 parser.add_option('--diff', action='store_true',
5029 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005030 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005031
Daniel Chengc55eecf2016-12-30 03:11:02 -08005032 # Normalize any remaining args against the current path, so paths relative to
5033 # the current directory are still resolved as expected.
5034 args = [os.path.join(os.getcwd(), arg) for arg in args]
5035
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005036 # git diff generates paths against the root of the repository. Change
5037 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005038 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005039 if rel_base_path:
5040 os.chdir(rel_base_path)
5041
digit@chromium.org29e47272013-05-17 17:01:46 +00005042 # Grab the merge-base commit, i.e. the upstream commit of the current
5043 # branch when it was created or the last time it was rebased. This is
5044 # to cover the case where the user may have called "git fetch origin",
5045 # moving the origin branch to a newer commit, but hasn't rebased yet.
5046 upstream_commit = None
5047 cl = Changelist()
5048 upstream_branch = cl.GetUpstreamBranch()
5049 if upstream_branch:
5050 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5051 upstream_commit = upstream_commit.strip()
5052
5053 if not upstream_commit:
5054 DieWithError('Could not find base commit for this branch. '
5055 'Are you in detached state?')
5056
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005057 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5058 diff_output = RunGit(changed_files_cmd)
5059 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005060 # Filter out files deleted by this CL
5061 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005062
Christopher Lamc5ba6922017-01-24 11:19:14 +11005063 if opts.js:
5064 CLANG_EXTS.append('.js')
5065
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005066 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5067 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5068 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005069 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005070
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005071 top_dir = os.path.normpath(
5072 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5073
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005074 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5075 # formatted. This is used to block during the presubmit.
5076 return_value = 0
5077
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005078 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005079 # Locate the clang-format binary in the checkout
5080 try:
5081 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005082 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005083 DieWithError(e)
5084
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005085 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005086 cmd = [clang_format_tool]
5087 if not opts.dry_run and not opts.diff:
5088 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005089 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005090 if opts.diff:
5091 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005092 else:
5093 env = os.environ.copy()
5094 env['PATH'] = str(os.path.dirname(clang_format_tool))
5095 try:
5096 script = clang_format.FindClangFormatScriptInChromiumTree(
5097 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005098 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005099 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005100
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005101 cmd = [sys.executable, script, '-p0']
5102 if not opts.dry_run and not opts.diff:
5103 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005104
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005105 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5106 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005107
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005108 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5109 if opts.diff:
5110 sys.stdout.write(stdout)
5111 if opts.dry_run and len(stdout) > 0:
5112 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005113
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005114 # Similar code to above, but using yapf on .py files rather than clang-format
5115 # on C/C++ files
5116 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005117 yapf_tool = gclient_utils.FindExecutable('yapf')
5118 if yapf_tool is None:
5119 DieWithError('yapf not found in PATH')
5120
5121 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005122 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005123 cmd = [yapf_tool]
5124 if not opts.dry_run and not opts.diff:
5125 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005126 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005127 if opts.diff:
5128 sys.stdout.write(stdout)
5129 else:
5130 # TODO(sbc): yapf --lines mode still has some issues.
5131 # https://github.com/google/yapf/issues/154
5132 DieWithError('--python currently only works with --full')
5133
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005134 # Dart's formatter does not have the nice property of only operating on
5135 # modified chunks, so hard code full.
5136 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005137 try:
5138 command = [dart_format.FindDartFmtToolInChromiumTree()]
5139 if not opts.dry_run and not opts.diff:
5140 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005141 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005142
ppi@chromium.org6593d932016-03-03 15:41:15 +00005143 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005144 if opts.dry_run and stdout:
5145 return_value = 2
5146 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005147 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5148 'found in this checkout. Files in other languages are still '
5149 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005150
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005151 # Format GN build files. Always run on full build files for canonical form.
5152 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005153 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005154 if opts.dry_run or opts.diff:
5155 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005156 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005157 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5158 shell=sys.platform == 'win32',
5159 cwd=top_dir)
5160 if opts.dry_run and gn_ret == 2:
5161 return_value = 2 # Not formatted.
5162 elif opts.diff and gn_ret == 2:
5163 # TODO this should compute and print the actual diff.
5164 print("This change has GN build file diff for " + gn_diff_file)
5165 elif gn_ret != 0:
5166 # For non-dry run cases (and non-2 return values for dry-run), a
5167 # nonzero error code indicates a failure, probably because the file
5168 # doesn't parse.
5169 DieWithError("gn format failed on " + gn_diff_file +
5170 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005171
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005172 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005173
5174
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005175@subcommand.usage('<codereview url or issue id>')
5176def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005177 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005178 _, args = parser.parse_args(args)
5179
5180 if len(args) != 1:
5181 parser.print_help()
5182 return 1
5183
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005184 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005185 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005186 parser.print_help()
5187 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005188 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005189
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005190 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005191 output = RunGit(['config', '--local', '--get-regexp',
5192 r'branch\..*\.%s' % issueprefix],
5193 error_ok=True)
5194 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005195 if issue == target_issue:
5196 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005197
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005198 branches = []
5199 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005200 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005201 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005202 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005203 return 1
5204 if len(branches) == 1:
5205 RunGit(['checkout', branches[0]])
5206 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005207 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005208 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005209 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005210 which = raw_input('Choose by index: ')
5211 try:
5212 RunGit(['checkout', branches[int(which)]])
5213 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005214 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005215 return 1
5216
5217 return 0
5218
5219
maruel@chromium.org29404b52014-09-08 22:58:00 +00005220def CMDlol(parser, args):
5221 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005222 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005223 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5224 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5225 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005226 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005227 return 0
5228
5229
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005230class OptionParser(optparse.OptionParser):
5231 """Creates the option parse and add --verbose support."""
5232 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005233 optparse.OptionParser.__init__(
5234 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005235 self.add_option(
5236 '-v', '--verbose', action='count', default=0,
5237 help='Use 2 times for more debugging info')
5238
5239 def parse_args(self, args=None, values=None):
5240 options, args = optparse.OptionParser.parse_args(self, args, values)
5241 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005242 logging.basicConfig(
5243 level=levels[min(options.verbose, len(levels) - 1)],
5244 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5245 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005246 return options, args
5247
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005248
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005249def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005250 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005251 print('\nYour python version %s is unsupported, please upgrade.\n' %
5252 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005253 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005254
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005255 # Reload settings.
5256 global settings
5257 settings = Settings()
5258
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005259 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005260 dispatcher = subcommand.CommandDispatcher(__name__)
5261 try:
5262 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005263 except auth.AuthenticationError as e:
5264 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005265 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005266 if e.code != 500:
5267 raise
5268 DieWithError(
5269 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5270 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005271 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005272
5273
5274if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005275 # These affect sys.stdout so do it outside of main() to simplify mocks in
5276 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005277 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005278 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005279 try:
5280 sys.exit(main(sys.argv[1:]))
5281 except KeyboardInterrupt:
5282 sys.stderr.write('interrupted\n')
5283 sys.exit(1)