blob: 1deebe3d3f34e603e5e3c48a167ecbbe35321f72 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010017import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000018import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000019import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000021import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import optparse
23import os
24import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000025import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080036 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
skobes6468b902016-10-24 08:45:10 -070044import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080067POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
borenet6c0efe62016-10-19 08:13:29 -070078# Buildbucket master name prefix.
79MASTER_PREFIX = 'master.'
80
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000081# Shortcut since it quickly becomes redundant.
82Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000083
maruel@chromium.orgddd59412011-11-30 14:20:38 +000084# Initialized in main()
85settings = None
86
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010087# Used by tests/git_cl_test.py to add extra logging.
88# Inside the weirdly failing test, add this:
89# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
90# And scroll up to see the strack trace printed.
91_IS_BEING_TESTED = False
92
maruel@chromium.orgddd59412011-11-30 14:20:38 +000093
Christopher Lamf732cd52017-01-24 12:40:11 +110094def DieWithError(message, change_desc=None):
95 if change_desc:
96 SaveDescriptionBackup(change_desc)
97
vapiera7fbd5a2016-06-16 09:17:49 -070098 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 sys.exit(1)
100
101
Christopher Lamf732cd52017-01-24 12:40:11 +1100102def SaveDescriptionBackup(change_desc):
103 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
104 print('\nError after CL description prompt -- saving description to %s\n' %
105 backup_path)
106 backup_file = open(backup_path, 'w')
107 backup_file.write(change_desc.description)
108 backup_file.close()
109
110
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000111def GetNoGitPagerEnv():
112 env = os.environ.copy()
113 # 'cat' is a magical git string that disables pagers on all platforms.
114 env['GIT_PAGER'] = 'cat'
115 return env
116
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000117
bsep@chromium.org627d9002016-04-29 00:00:52 +0000118def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000119 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000120 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000121 except subprocess2.CalledProcessError as e:
122 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000123 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000124 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000125 'Command "%s" failed.\n%s' % (
126 ' '.join(args), error_message or e.stdout or ''))
127 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128
129
130def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000131 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000132 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000135def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700137 if suppress_stderr:
138 stderr = subprocess2.VOID
139 else:
140 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000141 try:
tandrii5d48c322016-08-18 16:19:37 -0700142 (out, _), code = subprocess2.communicate(['git'] + args,
143 env=GetNoGitPagerEnv(),
144 stdout=subprocess2.PIPE,
145 stderr=stderr)
146 return code, out
147 except subprocess2.CalledProcessError as e:
148 logging.debug('Failed running %s', args)
149 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000150
151
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000152def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000153 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000154 return RunGitWithCode(args, suppress_stderr=True)[1]
155
156
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000157def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000158 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000159 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000160 return (version.startswith(prefix) and
161 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162
163
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000164def BranchExists(branch):
165 """Return True if specified branch exists."""
166 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
167 suppress_stderr=True)
168 return not code
169
170
tandrii2a16b952016-10-19 07:09:44 -0700171def time_sleep(seconds):
172 # Use this so that it can be mocked in tests without interfering with python
173 # system machinery.
174 import time # Local import to discourage others from importing time globally.
175 return time.sleep(seconds)
176
177
maruel@chromium.org90541732011-04-01 17:54:18 +0000178def ask_for_data(prompt):
179 try:
180 return raw_input(prompt)
181 except KeyboardInterrupt:
182 # Hide the exception.
183 sys.exit(1)
184
185
tandrii5d48c322016-08-18 16:19:37 -0700186def _git_branch_config_key(branch, key):
187 """Helper method to return Git config key for a branch."""
188 assert branch, 'branch name is required to set git config for it'
189 return 'branch.%s.%s' % (branch, key)
190
191
192def _git_get_branch_config_value(key, default=None, value_type=str,
193 branch=False):
194 """Returns git config value of given or current branch if any.
195
196 Returns default in all other cases.
197 """
198 assert value_type in (int, str, bool)
199 if branch is False: # Distinguishing default arg value from None.
200 branch = GetCurrentBranch()
201
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000202 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700203 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000204
tandrii5d48c322016-08-18 16:19:37 -0700205 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700206 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700207 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700208 # git config also has --int, but apparently git config suffers from integer
209 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700210 args.append(_git_branch_config_key(branch, key))
211 code, out = RunGitWithCode(args)
212 if code == 0:
213 value = out.strip()
214 if value_type == int:
215 return int(value)
216 if value_type == bool:
217 return bool(value.lower() == 'true')
218 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000219 return default
220
221
tandrii5d48c322016-08-18 16:19:37 -0700222def _git_set_branch_config_value(key, value, branch=None, **kwargs):
223 """Sets the value or unsets if it's None of a git branch config.
224
225 Valid, though not necessarily existing, branch must be provided,
226 otherwise currently checked out branch is used.
227 """
228 if not branch:
229 branch = GetCurrentBranch()
230 assert branch, 'a branch name OR currently checked out branch is required'
231 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700232 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700233 if value is None:
234 args.append('--unset')
235 elif isinstance(value, bool):
236 args.append('--bool')
237 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700238 else:
tandrii33a46ff2016-08-23 05:53:40 -0700239 # git config also has --int, but apparently git config suffers from integer
240 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700241 value = str(value)
242 args.append(_git_branch_config_key(branch, key))
243 if value is not None:
244 args.append(value)
245 RunGit(args, **kwargs)
246
247
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100248def _get_committer_timestamp(commit):
249 """Returns unix timestamp as integer of a committer in a commit.
250
251 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
252 """
253 # Git also stores timezone offset, but it only affects visual display,
254 # actual point in time is defined by this timestamp only.
255 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
256
257
258def _git_amend_head(message, committer_timestamp):
259 """Amends commit with new message and desired committer_timestamp.
260
261 Sets committer timezone to UTC.
262 """
263 env = os.environ.copy()
264 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
265 return RunGit(['commit', '--amend', '-m', message], env=env)
266
267
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000268def add_git_similarity(parser):
269 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700270 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000271 help='Sets the percentage that a pair of files need to match in order to'
272 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000273 parser.add_option(
274 '--find-copies', action='store_true',
275 help='Allows git to look for copies.')
276 parser.add_option(
277 '--no-find-copies', action='store_false', dest='find_copies',
278 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000279
280 old_parser_args = parser.parse_args
281 def Parse(args):
282 options, args = old_parser_args(args)
283
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000284 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700285 options.similarity = _git_get_branch_config_value(
286 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000287 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000288 print('Note: Saving similarity of %d%% in git config.'
289 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700290 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000291
iannucci@chromium.org79540052012-10-19 23:15:26 +0000292 options.similarity = max(0, min(options.similarity, 100))
293
294 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700295 options.find_copies = _git_get_branch_config_value(
296 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000297 else:
tandrii5d48c322016-08-18 16:19:37 -0700298 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000299
300 print('Using %d%% similarity for rename/copy detection. '
301 'Override with --similarity.' % options.similarity)
302
303 return options, args
304 parser.parse_args = Parse
305
306
machenbach@chromium.org45453142015-09-15 08:45:22 +0000307def _get_properties_from_options(options):
308 properties = dict(x.split('=', 1) for x in options.properties)
309 for key, val in properties.iteritems():
310 try:
311 properties[key] = json.loads(val)
312 except ValueError:
313 pass # If a value couldn't be evaluated, treat it as a string.
314 return properties
315
316
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000317def _prefix_master(master):
318 """Convert user-specified master name to full master name.
319
320 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
321 name, while the developers always use shortened master name
322 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
323 function does the conversion for buildbucket migration.
324 """
borenet6c0efe62016-10-19 08:13:29 -0700325 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000326 return master
borenet6c0efe62016-10-19 08:13:29 -0700327 return '%s%s' % (MASTER_PREFIX, master)
328
329
330def _unprefix_master(bucket):
331 """Convert bucket name to shortened master name.
332
333 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
334 name, while the developers always use shortened master name
335 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
336 function does the conversion for buildbucket migration.
337 """
338 if bucket.startswith(MASTER_PREFIX):
339 return bucket[len(MASTER_PREFIX):]
340 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000341
342
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000343def _buildbucket_retry(operation_name, http, *args, **kwargs):
344 """Retries requests to buildbucket service and returns parsed json content."""
345 try_count = 0
346 while True:
347 response, content = http.request(*args, **kwargs)
348 try:
349 content_json = json.loads(content)
350 except ValueError:
351 content_json = None
352
353 # Buildbucket could return an error even if status==200.
354 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000355 error = content_json.get('error')
356 if error.get('code') == 403:
357 raise BuildbucketResponseException(
358 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000360 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000361 raise BuildbucketResponseException(msg)
362
363 if response.status == 200:
364 if not content_json:
365 raise BuildbucketResponseException(
366 'Buildbucket returns invalid json content: %s.\n'
367 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
368 content)
369 return content_json
370 if response.status < 500 or try_count >= 2:
371 raise httplib2.HttpLib2Error(content)
372
373 # status >= 500 means transient failures.
374 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700375 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000376 try_count += 1
377 assert False, 'unreachable'
378
379
qyearsley1fdfcb62016-10-24 13:22:03 -0700380def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700381 """Returns a dict mapping bucket names to builders and tests,
382 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700383 """
qyearsleydd49f942016-10-28 11:57:22 -0700384 # If no bots are listed, we try to get a set of builders and tests based
385 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700386 if not options.bot:
387 change = changelist.GetChange(
388 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700389 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700390 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700391 change=change,
392 changed_files=change.LocalPaths(),
393 repository_root=settings.GetRoot(),
394 default_presubmit=None,
395 project=None,
396 verbose=options.verbose,
397 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700398 if masters is None:
399 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100400 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700401
qyearsley1fdfcb62016-10-24 13:22:03 -0700402 if options.bucket:
403 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700404 if options.master:
405 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
qyearsleydd49f942016-10-28 11:57:22 -0700407 # If bots are listed but no master or bucket, then we need to find out
408 # the corresponding master for each bot.
409 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
410 if error_message:
411 option_parser.error(
412 'Tryserver master cannot be found because: %s\n'
413 'Please manually specify the tryserver master, e.g. '
414 '"-m tryserver.chromium.linux".' % error_message)
415 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700416
417
qyearsley123a4682016-10-26 09:12:17 -0700418def _get_bucket_map_for_builders(builders):
419 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700420 map_url = 'https://builders-map.appspot.com/'
421 try:
qyearsley123a4682016-10-26 09:12:17 -0700422 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700423 except urllib2.URLError as e:
424 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
425 (map_url, e))
426 except ValueError as e:
427 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700428 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700429 return None, 'Failed to build master map.'
430
qyearsley123a4682016-10-26 09:12:17 -0700431 bucket_map = {}
432 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700433 masters = builders_map.get(builder, [])
434 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700435 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700436 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700437 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700438 (builder, masters))
439 bucket = _prefix_master(masters[0])
440 bucket_map.setdefault(bucket, {})[builder] = []
441
442 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700443
444
borenet6c0efe62016-10-19 08:13:29 -0700445def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700446 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700447 """Sends a request to Buildbucket to trigger try jobs for a changelist.
448
449 Args:
450 auth_config: AuthConfig for Rietveld.
451 changelist: Changelist that the try jobs are associated with.
452 buckets: A nested dict mapping bucket names to builders to tests.
453 options: Command-line options.
454 """
tandriide281ae2016-10-12 06:02:30 -0700455 assert changelist.GetIssue(), 'CL must be uploaded first'
456 codereview_url = changelist.GetCodereviewServer()
457 assert codereview_url, 'CL must be uploaded first'
458 patchset = patchset or changelist.GetMostRecentPatchset()
459 assert patchset, 'CL must be uploaded first'
460
461 codereview_host = urlparse.urlparse(codereview_url).hostname
462 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000463 http = authenticator.authorize(httplib2.Http())
464 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700465
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000466 buildbucket_put_url = (
467 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000468 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700469 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
470 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
471 hostname=codereview_host,
472 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000473 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700474
475 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
476 shared_parameters_properties['category'] = category
477 if options.clobber:
478 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700479 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700480 if extra_properties:
481 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000482
483 batch_req_body = {'builds': []}
484 print_text = []
485 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700486 for bucket, builders_and_tests in sorted(buckets.iteritems()):
487 print_text.append('Bucket: %s' % bucket)
488 master = None
489 if bucket.startswith(MASTER_PREFIX):
490 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000491 for builder, tests in sorted(builders_and_tests.iteritems()):
492 print_text.append(' %s: %s' % (builder, tests))
493 parameters = {
494 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000495 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100496 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000497 'revision': options.revision,
498 }],
tandrii8c5a3532016-11-04 07:52:02 -0700499 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000500 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000501 if 'presubmit' in builder.lower():
502 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000503 if tests:
504 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700505
506 tags = [
507 'builder:%s' % builder,
508 'buildset:%s' % buildset,
509 'user_agent:git_cl_try',
510 ]
511 if master:
512 parameters['properties']['master'] = master
513 tags.append('master:%s' % master)
514
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000515 batch_req_body['builds'].append(
516 {
517 'bucket': bucket,
518 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000519 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700520 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000521 }
522 )
523
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000524 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700525 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000526 http,
527 buildbucket_put_url,
528 'PUT',
529 body=json.dumps(batch_req_body),
530 headers={'Content-Type': 'application/json'}
531 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000532 print_text.append('To see results here, run: git cl try-results')
533 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700534 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000535
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000536
tandrii221ab252016-10-06 08:12:04 -0700537def fetch_try_jobs(auth_config, changelist, buildbucket_host,
538 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700539 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000540
qyearsley53f48a12016-09-01 10:45:13 -0700541 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000542 """
tandrii221ab252016-10-06 08:12:04 -0700543 assert buildbucket_host
544 assert changelist.GetIssue(), 'CL must be uploaded first'
545 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
546 patchset = patchset or changelist.GetMostRecentPatchset()
547 assert patchset, 'CL must be uploaded first'
548
549 codereview_url = changelist.GetCodereviewServer()
550 codereview_host = urlparse.urlparse(codereview_url).hostname
551 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 if authenticator.has_cached_credentials():
553 http = authenticator.authorize(httplib2.Http())
554 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700555 print('Warning: Some results might be missing because %s' %
556 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700557 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 http = httplib2.Http()
559
560 http.force_exception_to_status_code = True
561
tandrii221ab252016-10-06 08:12:04 -0700562 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
563 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
564 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700566 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567 params = {'tag': 'buildset:%s' % buildset}
568
569 builds = {}
570 while True:
571 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700572 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700574 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000575 for build in content.get('builds', []):
576 builds[build['id']] = build
577 if 'next_cursor' in content:
578 params['start_cursor'] = content['next_cursor']
579 else:
580 break
581 return builds
582
583
qyearsleyeab3c042016-08-24 09:18:28 -0700584def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 """Prints nicely result of fetch_try_jobs."""
586 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700587 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000588 return
589
590 # Make a copy, because we'll be modifying builds dictionary.
591 builds = builds.copy()
592 builder_names_cache = {}
593
594 def get_builder(b):
595 try:
596 return builder_names_cache[b['id']]
597 except KeyError:
598 try:
599 parameters = json.loads(b['parameters_json'])
600 name = parameters['builder_name']
601 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700602 print('WARNING: failed to get builder name for build %s: %s' % (
603 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000604 name = None
605 builder_names_cache[b['id']] = name
606 return name
607
608 def get_bucket(b):
609 bucket = b['bucket']
610 if bucket.startswith('master.'):
611 return bucket[len('master.'):]
612 return bucket
613
614 if options.print_master:
615 name_fmt = '%%-%ds %%-%ds' % (
616 max(len(str(get_bucket(b))) for b in builds.itervalues()),
617 max(len(str(get_builder(b))) for b in builds.itervalues()))
618 def get_name(b):
619 return name_fmt % (get_bucket(b), get_builder(b))
620 else:
621 name_fmt = '%%-%ds' % (
622 max(len(str(get_builder(b))) for b in builds.itervalues()))
623 def get_name(b):
624 return name_fmt % get_builder(b)
625
626 def sort_key(b):
627 return b['status'], b.get('result'), get_name(b), b.get('url')
628
629 def pop(title, f, color=None, **kwargs):
630 """Pop matching builds from `builds` dict and print them."""
631
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000632 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000633 colorize = str
634 else:
635 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
636
637 result = []
638 for b in builds.values():
639 if all(b.get(k) == v for k, v in kwargs.iteritems()):
640 builds.pop(b['id'])
641 result.append(b)
642 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700643 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000644 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700645 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000646
647 total = len(builds)
648 pop(status='COMPLETED', result='SUCCESS',
649 title='Successes:', color=Fore.GREEN,
650 f=lambda b: (get_name(b), b.get('url')))
651 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
652 title='Infra Failures:', color=Fore.MAGENTA,
653 f=lambda b: (get_name(b), b.get('url')))
654 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
655 title='Failures:', color=Fore.RED,
656 f=lambda b: (get_name(b), b.get('url')))
657 pop(status='COMPLETED', result='CANCELED',
658 title='Canceled:', color=Fore.MAGENTA,
659 f=lambda b: (get_name(b),))
660 pop(status='COMPLETED', result='FAILURE',
661 failure_reason='INVALID_BUILD_DEFINITION',
662 title='Wrong master/builder name:', color=Fore.MAGENTA,
663 f=lambda b: (get_name(b),))
664 pop(status='COMPLETED', result='FAILURE',
665 title='Other failures:',
666 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
667 pop(status='COMPLETED',
668 title='Other finished:',
669 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
670 pop(status='STARTED',
671 title='Started:', color=Fore.YELLOW,
672 f=lambda b: (get_name(b), b.get('url')))
673 pop(status='SCHEDULED',
674 title='Scheduled:',
675 f=lambda b: (get_name(b), 'id=%s' % b['id']))
676 # The last section is just in case buildbucket API changes OR there is a bug.
677 pop(title='Other:',
678 f=lambda b: (get_name(b), 'id=%s' % b['id']))
679 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700680 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000681
682
qyearsley53f48a12016-09-01 10:45:13 -0700683def write_try_results_json(output_file, builds):
684 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
685
686 The input |builds| dict is assumed to be generated by Buildbucket.
687 Buildbucket documentation: http://goo.gl/G0s101
688 """
689
690 def convert_build_dict(build):
691 return {
692 'buildbucket_id': build.get('id'),
693 'status': build.get('status'),
694 'result': build.get('result'),
695 'bucket': build.get('bucket'),
696 'builder_name': json.loads(
697 build.get('parameters_json', '{}')).get('builder_name'),
698 'failure_reason': build.get('failure_reason'),
699 'url': build.get('url'),
700 }
701
702 converted = []
703 for _, build in sorted(builds.items()):
704 converted.append(convert_build_dict(build))
705 write_json(output_file, converted)
706
707
iannucci@chromium.org79540052012-10-19 23:15:26 +0000708def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000709 """Prints statistics about the change to the user."""
710 # --no-ext-diff is broken in some versions of Git, so try to work around
711 # this by overriding the environment (but there is still a problem if the
712 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000713 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000714 if 'GIT_EXTERNAL_DIFF' in env:
715 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000716
717 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800718 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000719 else:
720 similarity_options = ['-M%s' % similarity]
721
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000722 try:
723 stdout = sys.stdout.fileno()
724 except AttributeError:
725 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000726 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000727 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000728 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000729 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000730
731
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000732class BuildbucketResponseException(Exception):
733 pass
734
735
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736class Settings(object):
737 def __init__(self):
738 self.default_server = None
739 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000740 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741 self.tree_status_url = None
742 self.viewvc_url = None
743 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000744 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000745 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000746 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000747 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000748 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000749 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750
751 def LazyUpdateIfNeeded(self):
752 """Updates the settings from a codereview.settings file, if available."""
753 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000754 # The only value that actually changes the behavior is
755 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000756 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000757 error_ok=True
758 ).strip().lower()
759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000761 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762 LoadCodereviewSettingsFromFile(cr_settings_file)
763 self.updated = True
764
765 def GetDefaultServerUrl(self, error_ok=False):
766 if not self.default_server:
767 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000768 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000769 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 if error_ok:
771 return self.default_server
772 if not self.default_server:
773 error_message = ('Could not find settings file. You must configure '
774 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000775 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000776 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777 return self.default_server
778
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000779 @staticmethod
780 def GetRelativeRoot():
781 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000782
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000784 if self.root is None:
785 self.root = os.path.abspath(self.GetRelativeRoot())
786 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000788 def GetGitMirror(self, remote='origin'):
789 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000790 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000791 if not os.path.isdir(local_url):
792 return None
793 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
794 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100795 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100796 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000797 if mirror.exists():
798 return mirror
799 return None
800
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 def GetTreeStatusUrl(self, error_ok=False):
802 if not self.tree_status_url:
803 error_message = ('You must configure your tree status URL by running '
804 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 self.tree_status_url = self._GetRietveldConfig(
806 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 return self.tree_status_url
808
809 def GetViewVCUrl(self):
810 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 return self.viewvc_url
813
rmistry@google.com90752582014-01-14 21:04:50 +0000814 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000815 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000816
rmistry@google.com78948ed2015-07-08 23:09:57 +0000817 def GetIsSkipDependencyUpload(self, branch_name):
818 """Returns true if specified branch should skip dep uploads."""
819 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
820 error_ok=True)
821
rmistry@google.com5626a922015-02-26 14:03:30 +0000822 def GetRunPostUploadHook(self):
823 run_post_upload_hook = self._GetRietveldConfig(
824 'run-post-upload-hook', error_ok=True)
825 return run_post_upload_hook == "True"
826
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000827 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000828 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000829
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000830 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000831 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000832
ukai@chromium.orge8077812012-02-03 03:41:46 +0000833 def GetIsGerrit(self):
834 """Return true if this repo is assosiated with gerrit code review system."""
835 if self.is_gerrit is None:
836 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
837 return self.is_gerrit
838
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000839 def GetSquashGerritUploads(self):
840 """Return true if uploads to Gerrit should be squashed by default."""
841 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700842 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
843 if self.squash_gerrit_uploads is None:
844 # Default is squash now (http://crbug.com/611892#c23).
845 self.squash_gerrit_uploads = not (
846 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
847 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000848 return self.squash_gerrit_uploads
849
tandriia60502f2016-06-20 02:01:53 -0700850 def GetSquashGerritUploadsOverride(self):
851 """Return True or False if codereview.settings should be overridden.
852
853 Returns None if no override has been defined.
854 """
855 # See also http://crbug.com/611892#c23
856 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
857 error_ok=True).strip()
858 if result == 'true':
859 return True
860 if result == 'false':
861 return False
862 return None
863
tandrii@chromium.org28253532016-04-14 13:46:56 +0000864 def GetGerritSkipEnsureAuthenticated(self):
865 """Return True if EnsureAuthenticated should not be done for Gerrit
866 uploads."""
867 if self.gerrit_skip_ensure_authenticated is None:
868 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000869 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000870 error_ok=True).strip() == 'true')
871 return self.gerrit_skip_ensure_authenticated
872
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000873 def GetGitEditor(self):
874 """Return the editor specified in the git config, or None if none is."""
875 if self.git_editor is None:
876 self.git_editor = self._GetConfig('core.editor', error_ok=True)
877 return self.git_editor or None
878
thestig@chromium.org44202a22014-03-11 19:22:18 +0000879 def GetLintRegex(self):
880 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
881 DEFAULT_LINT_REGEX)
882
883 def GetLintIgnoreRegex(self):
884 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
885 DEFAULT_LINT_IGNORE_REGEX)
886
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000887 def GetProject(self):
888 if not self.project:
889 self.project = self._GetRietveldConfig('project', error_ok=True)
890 return self.project
891
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000892 def _GetRietveldConfig(self, param, **kwargs):
893 return self._GetConfig('rietveld.' + param, **kwargs)
894
rmistry@google.com78948ed2015-07-08 23:09:57 +0000895 def _GetBranchConfig(self, branch_name, param, **kwargs):
896 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
897
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898 def _GetConfig(self, param, **kwargs):
899 self.LazyUpdateIfNeeded()
900 return RunGit(['config', param], **kwargs).strip()
901
902
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100903@contextlib.contextmanager
904def _get_gerrit_project_config_file(remote_url):
905 """Context manager to fetch and store Gerrit's project.config from
906 refs/meta/config branch and store it in temp file.
907
908 Provides a temporary filename or None if there was error.
909 """
910 error, _ = RunGitWithCode([
911 'fetch', remote_url,
912 '+refs/meta/config:refs/git_cl/meta/config'])
913 if error:
914 # Ref doesn't exist or isn't accessible to current user.
915 print('WARNING: failed to fetch project config for %s: %s' %
916 (remote_url, error))
917 yield None
918 return
919
920 error, project_config_data = RunGitWithCode(
921 ['show', 'refs/git_cl/meta/config:project.config'])
922 if error:
923 print('WARNING: project.config file not found')
924 yield None
925 return
926
927 with gclient_utils.temporary_directory() as tempdir:
928 project_config_file = os.path.join(tempdir, 'project.config')
929 gclient_utils.FileWrite(project_config_file, project_config_data)
930 yield project_config_file
931
932
933def _is_git_numberer_enabled(remote_url, remote_ref):
934 """Returns True if Git Numberer is enabled on this ref."""
935 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100936 KNOWN_PROJECTS_WHITELIST = [
937 'chromium/src',
938 'external/webrtc',
939 'v8/v8',
940 ]
941
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100942 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
943 url_parts = urlparse.urlparse(remote_url)
944 project_name = url_parts.path.lstrip('/').rstrip('git./')
945 for known in KNOWN_PROJECTS_WHITELIST:
946 if project_name.endswith(known):
947 break
948 else:
949 # Early exit to avoid extra fetches for repos that aren't using Git
950 # Numberer.
951 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100952
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100953 with _get_gerrit_project_config_file(remote_url) as project_config_file:
954 if project_config_file is None:
955 # Failed to fetch project.config, which shouldn't happen on open source
956 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100957 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100958 def get_opts(x):
959 code, out = RunGitWithCode(
960 ['config', '-f', project_config_file, '--get-all',
961 'plugin.git-numberer.validate-%s-refglob' % x])
962 if code == 0:
963 return out.strip().splitlines()
964 return []
965 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100966
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100967 logging.info('validator config enabled %s disabled %s refglobs for '
968 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000969
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100970 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100971 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100972 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100973 return True
974 return False
975
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100976 if match_refglobs(disabled):
977 return False
978 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100979
980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981def ShortBranchName(branch):
982 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000983 return branch.replace('refs/heads/', '', 1)
984
985
986def GetCurrentBranchRef():
987 """Returns branch ref (e.g., refs/heads/master) or None."""
988 return RunGit(['symbolic-ref', 'HEAD'],
989 stderr=subprocess2.VOID, error_ok=True).strip() or None
990
991
992def GetCurrentBranch():
993 """Returns current branch or None.
994
995 For refs/heads/* branches, returns just last part. For others, full ref.
996 """
997 branchref = GetCurrentBranchRef()
998 if branchref:
999 return ShortBranchName(branchref)
1000 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001
1002
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001003class _CQState(object):
1004 """Enum for states of CL with respect to Commit Queue."""
1005 NONE = 'none'
1006 DRY_RUN = 'dry_run'
1007 COMMIT = 'commit'
1008
1009 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1010
1011
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001012class _ParsedIssueNumberArgument(object):
1013 def __init__(self, issue=None, patchset=None, hostname=None):
1014 self.issue = issue
1015 self.patchset = patchset
1016 self.hostname = hostname
1017
1018 @property
1019 def valid(self):
1020 return self.issue is not None
1021
1022
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001023def ParseIssueNumberArgument(arg):
1024 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1025 fail_result = _ParsedIssueNumberArgument()
1026
1027 if arg.isdigit():
1028 return _ParsedIssueNumberArgument(issue=int(arg))
1029 if not arg.startswith('http'):
1030 return fail_result
1031 url = gclient_utils.UpgradeToHttps(arg)
1032 try:
1033 parsed_url = urlparse.urlparse(url)
1034 except ValueError:
1035 return fail_result
1036 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1037 tmp = cls.ParseIssueURL(parsed_url)
1038 if tmp is not None:
1039 return tmp
1040 return fail_result
1041
1042
Aaron Gablea45ee112016-11-22 15:14:38 -08001043class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001044 def __init__(self, issue, url):
1045 self.issue = issue
1046 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001047 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001048
1049 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001050 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001051 self.issue, self.url)
1052
1053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001055 """Changelist works with one changelist in local branch.
1056
1057 Supports two codereview backends: Rietveld or Gerrit, selected at object
1058 creation.
1059
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001060 Notes:
1061 * Not safe for concurrent multi-{thread,process} use.
1062 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001063 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001064 """
1065
1066 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1067 """Create a new ChangeList instance.
1068
1069 If issue is given, the codereview must be given too.
1070
1071 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1072 Otherwise, it's decided based on current configuration of the local branch,
1073 with default being 'rietveld' for backwards compatibility.
1074 See _load_codereview_impl for more details.
1075
1076 **kwargs will be passed directly to codereview implementation.
1077 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001079 global settings
1080 if not settings:
1081 # Happens when git_cl.py is used as a utility library.
1082 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001083
1084 if issue:
1085 assert codereview, 'codereview must be known, if issue is known'
1086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087 self.branchref = branchref
1088 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001089 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 self.branch = ShortBranchName(self.branchref)
1091 else:
1092 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001094 self.lookedup_issue = False
1095 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001096 self.has_description = False
1097 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001098 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001100 self.cc = None
1101 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001102 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001103
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001104 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001105 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001106 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001107 assert self._codereview_impl
1108 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001109
1110 def _load_codereview_impl(self, codereview=None, **kwargs):
1111 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001112 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1113 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1114 self._codereview = codereview
1115 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116 return
1117
1118 # Automatic selection based on issue number set for a current branch.
1119 # Rietveld takes precedence over Gerrit.
1120 assert not self.issue
1121 # Whether we find issue or not, we are doing the lookup.
1122 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001123 if self.GetBranch():
1124 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1125 issue = _git_get_branch_config_value(
1126 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1127 if issue:
1128 self._codereview = codereview
1129 self._codereview_impl = cls(self, **kwargs)
1130 self.issue = int(issue)
1131 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001132
1133 # No issue is set for this branch, so decide based on repo-wide settings.
1134 return self._load_codereview_impl(
1135 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1136 **kwargs)
1137
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001138 def IsGerrit(self):
1139 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001140
1141 def GetCCList(self):
1142 """Return the users cc'd on this CL.
1143
agable92bec4f2016-08-24 09:27:27 -07001144 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001145 """
1146 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001147 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001148 more_cc = ','.join(self.watchers)
1149 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1150 return self.cc
1151
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001152 def GetCCListWithoutDefault(self):
1153 """Return the users cc'd on this CL excluding default ones."""
1154 if self.cc is None:
1155 self.cc = ','.join(self.watchers)
1156 return self.cc
1157
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001158 def SetWatchers(self, watchers):
1159 """Set the list of email addresses that should be cc'd based on the changed
1160 files in this CL.
1161 """
1162 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163
1164 def GetBranch(self):
1165 """Returns the short branch name, e.g. 'master'."""
1166 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001167 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001168 if not branchref:
1169 return None
1170 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171 self.branch = ShortBranchName(self.branchref)
1172 return self.branch
1173
1174 def GetBranchRef(self):
1175 """Returns the full branch name, e.g. 'refs/heads/master'."""
1176 self.GetBranch() # Poke the lazy loader.
1177 return self.branchref
1178
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001179 def ClearBranch(self):
1180 """Clears cached branch data of this object."""
1181 self.branch = self.branchref = None
1182
tandrii5d48c322016-08-18 16:19:37 -07001183 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1184 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1185 kwargs['branch'] = self.GetBranch()
1186 return _git_get_branch_config_value(key, default, **kwargs)
1187
1188 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1189 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1190 assert self.GetBranch(), (
1191 'this CL must have an associated branch to %sset %s%s' %
1192 ('un' if value is None else '',
1193 key,
1194 '' if value is None else ' to %r' % value))
1195 kwargs['branch'] = self.GetBranch()
1196 return _git_set_branch_config_value(key, value, **kwargs)
1197
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001198 @staticmethod
1199 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001200 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 e.g. 'origin', 'refs/heads/master'
1202 """
1203 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001204 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1205
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001207 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001209 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1210 error_ok=True).strip()
1211 if upstream_branch:
1212 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001214 # Else, try to guess the origin remote.
1215 remote_branches = RunGit(['branch', '-r']).split()
1216 if 'origin/master' in remote_branches:
1217 # Fall back on origin/master if it exits.
1218 remote = 'origin'
1219 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001221 DieWithError(
1222 'Unable to determine default branch to diff against.\n'
1223 'Either pass complete "git diff"-style arguments, like\n'
1224 ' git cl upload origin/master\n'
1225 'or verify this branch is set up to track another \n'
1226 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227
1228 return remote, upstream_branch
1229
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001230 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001231 upstream_branch = self.GetUpstreamBranch()
1232 if not BranchExists(upstream_branch):
1233 DieWithError('The upstream for the current branch (%s) does not exist '
1234 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001235 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001236 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001237
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 def GetUpstreamBranch(self):
1239 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001240 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001242 upstream_branch = upstream_branch.replace('refs/heads/',
1243 'refs/remotes/%s/' % remote)
1244 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1245 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 self.upstream_branch = upstream_branch
1247 return self.upstream_branch
1248
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001249 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001250 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001251 remote, branch = None, self.GetBranch()
1252 seen_branches = set()
1253 while branch not in seen_branches:
1254 seen_branches.add(branch)
1255 remote, branch = self.FetchUpstreamTuple(branch)
1256 branch = ShortBranchName(branch)
1257 if remote != '.' or branch.startswith('refs/remotes'):
1258 break
1259 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001260 remotes = RunGit(['remote'], error_ok=True).split()
1261 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001262 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001263 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001264 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001265 logging.warn('Could not determine which remote this change is '
1266 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001267 else:
1268 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001269 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 branch = 'HEAD'
1271 if branch.startswith('refs/remotes'):
1272 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001273 elif branch.startswith('refs/branch-heads/'):
1274 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001275 else:
1276 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001277 return self._remote
1278
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001279 def GitSanityChecks(self, upstream_git_obj):
1280 """Checks git repo status and ensures diff is from local commits."""
1281
sbc@chromium.org79706062015-01-14 21:18:12 +00001282 if upstream_git_obj is None:
1283 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001284 print('ERROR: unable to determine current branch (detached HEAD?)',
1285 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001286 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001287 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001288 return False
1289
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 # Verify the commit we're diffing against is in our current branch.
1291 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1292 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1293 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001294 print('ERROR: %s is not in the current branch. You may need to rebase '
1295 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001296 return False
1297
1298 # List the commits inside the diff, and verify they are all local.
1299 commits_in_diff = RunGit(
1300 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1301 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1302 remote_branch = remote_branch.strip()
1303 if code != 0:
1304 _, remote_branch = self.GetRemoteBranch()
1305
1306 commits_in_remote = RunGit(
1307 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1308
1309 common_commits = set(commits_in_diff) & set(commits_in_remote)
1310 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001311 print('ERROR: Your diff contains %d commits already in %s.\n'
1312 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1313 'the diff. If you are using a custom git flow, you can override'
1314 ' the reference used for this check with "git config '
1315 'gitcl.remotebranch <git-ref>".' % (
1316 len(common_commits), remote_branch, upstream_git_obj),
1317 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001318 return False
1319 return True
1320
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001321 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001322 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001323
1324 Returns None if it is not set.
1325 """
tandrii5d48c322016-08-18 16:19:37 -07001326 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001327
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 def GetRemoteUrl(self):
1329 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1330
1331 Returns None if there is no remote.
1332 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001333 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001334 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1335
1336 # If URL is pointing to a local directory, it is probably a git cache.
1337 if os.path.isdir(url):
1338 url = RunGit(['config', 'remote.%s.url' % remote],
1339 error_ok=True,
1340 cwd=url).strip()
1341 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001343 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001344 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001345 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001346 self.issue = self._GitGetBranchConfigValue(
1347 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001348 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349 return self.issue
1350
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351 def GetIssueURL(self):
1352 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001353 issue = self.GetIssue()
1354 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001355 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001356 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001358 def GetDescription(self, pretty=False, force=False):
1359 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001361 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 self.has_description = True
1363 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001364 # Set width to 72 columns + 2 space indent.
1365 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001367 lines = self.description.splitlines()
1368 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 return self.description
1370
1371 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001372 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001373 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001374 self.patchset = self._GitGetBranchConfigValue(
1375 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001376 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 return self.patchset
1378
1379 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001380 """Set this branch's patchset. If patchset=0, clears the patchset."""
1381 assert self.GetBranch()
1382 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001383 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001384 else:
1385 self.patchset = int(patchset)
1386 self._GitSetBranchConfigValue(
1387 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001389 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001390 """Set this branch's issue. If issue isn't given, clears the issue."""
1391 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001393 issue = int(issue)
1394 self._GitSetBranchConfigValue(
1395 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001396 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001397 codereview_server = self._codereview_impl.GetCodereviewServer()
1398 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001399 self._GitSetBranchConfigValue(
1400 self._codereview_impl.CodereviewServerConfigKey(),
1401 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 else:
tandrii5d48c322016-08-18 16:19:37 -07001403 # Reset all of these just to be clean.
1404 reset_suffixes = [
1405 'last-upload-hash',
1406 self._codereview_impl.IssueConfigKey(),
1407 self._codereview_impl.PatchsetConfigKey(),
1408 self._codereview_impl.CodereviewServerConfigKey(),
1409 ] + self._PostUnsetIssueProperties()
1410 for prop in reset_suffixes:
1411 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001412 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001413 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414
dnjba1b0f32016-09-02 12:37:42 -07001415 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001416 if not self.GitSanityChecks(upstream_branch):
1417 DieWithError('\nGit sanity check failure')
1418
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001419 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001420 if not root:
1421 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001422 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001423
1424 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001425 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001426 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001427 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001428 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001429 except subprocess2.CalledProcessError:
1430 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001431 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001432 'This branch probably doesn\'t exist anymore. To reset the\n'
1433 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001434 ' git branch --set-upstream-to origin/master %s\n'
1435 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001436 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001437
maruel@chromium.org52424302012-08-29 15:14:30 +00001438 issue = self.GetIssue()
1439 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001440 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001441 description = self.GetDescription()
1442 else:
1443 # If the change was never uploaded, use the log messages of all commits
1444 # up to the branch point, as git cl upload will prefill the description
1445 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001446 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1447 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001448
1449 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001450 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001451 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001452 name,
1453 description,
1454 absroot,
1455 files,
1456 issue,
1457 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001458 author,
1459 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001460
dsansomee2d6fd92016-09-08 00:10:47 -07001461 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001462 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001463 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001464 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001465
1466 def RunHook(self, committing, may_prompt, verbose, change):
1467 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1468 try:
1469 return presubmit_support.DoPresubmitChecks(change, committing,
1470 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1471 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001472 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1473 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001474 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001475 DieWithError(
1476 ('%s\nMaybe your depot_tools is out of date?\n'
1477 'If all fails, contact maruel@') % e)
1478
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001479 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1480 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001481 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1482 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001483 else:
1484 # Assume url.
1485 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1486 urlparse.urlparse(issue_arg))
1487 if not parsed_issue_arg or not parsed_issue_arg.valid:
1488 DieWithError('Failed to parse issue argument "%s". '
1489 'Must be an issue number or a valid URL.' % issue_arg)
1490 return self._codereview_impl.CMDPatchWithParsedIssue(
1491 parsed_issue_arg, reject, nocommit, directory)
1492
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001493 def CMDUpload(self, options, git_diff_args, orig_args):
1494 """Uploads a change to codereview."""
1495 if git_diff_args:
1496 # TODO(ukai): is it ok for gerrit case?
1497 base_branch = git_diff_args[0]
1498 else:
1499 if self.GetBranch() is None:
1500 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1501
1502 # Default to diffing against common ancestor of upstream branch
1503 base_branch = self.GetCommonAncestorWithUpstream()
1504 git_diff_args = [base_branch, 'HEAD']
1505
1506 # Make sure authenticated to codereview before running potentially expensive
1507 # hooks. It is a fast, best efforts check. Codereview still can reject the
1508 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001509 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001510
1511 # Apply watchlists on upload.
1512 change = self.GetChange(base_branch, None)
1513 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1514 files = [f.LocalPath() for f in change.AffectedFiles()]
1515 if not options.bypass_watchlists:
1516 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1517
1518 if not options.bypass_hooks:
1519 if options.reviewers or options.tbr_owners:
1520 # Set the reviewer list now so that presubmit checks can access it.
1521 change_description = ChangeDescription(change.FullDescriptionText())
1522 change_description.update_reviewers(options.reviewers,
1523 options.tbr_owners,
1524 change)
1525 change.SetDescriptionText(change_description.description)
1526 hook_results = self.RunHook(committing=False,
1527 may_prompt=not options.force,
1528 verbose=options.verbose,
1529 change=change)
1530 if not hook_results.should_continue():
1531 return 1
1532 if not options.reviewers and hook_results.reviewers:
1533 options.reviewers = hook_results.reviewers.split(',')
1534
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001535 # TODO(tandrii): Checking local patchset against remote patchset is only
1536 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1537 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001538 latest_patchset = self.GetMostRecentPatchset()
1539 local_patchset = self.GetPatchset()
1540 if (latest_patchset and local_patchset and
1541 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001542 print('The last upload made from this repository was patchset #%d but '
1543 'the most recent patchset on the server is #%d.'
1544 % (local_patchset, latest_patchset))
1545 print('Uploading will still work, but if you\'ve uploaded to this '
1546 'issue from another machine or branch the patch you\'re '
1547 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 ask_for_data('About to upload; enter to confirm.')
1549
1550 print_stats(options.similarity, options.find_copies, git_diff_args)
1551 ret = self.CMDUploadChange(options, git_diff_args, change)
1552 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001553 if options.use_commit_queue:
1554 self.SetCQState(_CQState.COMMIT)
1555 elif options.cq_dry_run:
1556 self.SetCQState(_CQState.DRY_RUN)
1557
tandrii5d48c322016-08-18 16:19:37 -07001558 _git_set_branch_config_value('last-upload-hash',
1559 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001560 # Run post upload hooks, if specified.
1561 if settings.GetRunPostUploadHook():
1562 presubmit_support.DoPostUploadExecuter(
1563 change,
1564 self,
1565 settings.GetRoot(),
1566 options.verbose,
1567 sys.stdout)
1568
1569 # Upload all dependencies if specified.
1570 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001571 print()
1572 print('--dependencies has been specified.')
1573 print('All dependent local branches will be re-uploaded.')
1574 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001575 # Remove the dependencies flag from args so that we do not end up in a
1576 # loop.
1577 orig_args.remove('--dependencies')
1578 ret = upload_branch_deps(self, orig_args)
1579 return ret
1580
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001581 def SetCQState(self, new_state):
1582 """Update the CQ state for latest patchset.
1583
1584 Issue must have been already uploaded and known.
1585 """
1586 assert new_state in _CQState.ALL_STATES
1587 assert self.GetIssue()
1588 return self._codereview_impl.SetCQState(new_state)
1589
qyearsley1fdfcb62016-10-24 13:22:03 -07001590 def TriggerDryRun(self):
1591 """Triggers a dry run and prints a warning on failure."""
1592 # TODO(qyearsley): Either re-use this method in CMDset_commit
1593 # and CMDupload, or change CMDtry to trigger dry runs with
1594 # just SetCQState, and catch keyboard interrupt and other
1595 # errors in that method.
1596 try:
1597 self.SetCQState(_CQState.DRY_RUN)
1598 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1599 return 0
1600 except KeyboardInterrupt:
1601 raise
1602 except:
1603 print('WARNING: failed to trigger CQ Dry Run.\n'
1604 'Either:\n'
1605 ' * your project has no CQ\n'
1606 ' * you don\'t have permission to trigger Dry Run\n'
1607 ' * bug in this code (see stack trace below).\n'
1608 'Consider specifying which bots to trigger manually '
1609 'or asking your project owners for permissions '
1610 'or contacting Chrome Infrastructure team at '
1611 'https://www.chromium.org/infra\n\n')
1612 # Still raise exception so that stack trace is printed.
1613 raise
1614
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001615 # Forward methods to codereview specific implementation.
1616
1617 def CloseIssue(self):
1618 return self._codereview_impl.CloseIssue()
1619
1620 def GetStatus(self):
1621 return self._codereview_impl.GetStatus()
1622
1623 def GetCodereviewServer(self):
1624 return self._codereview_impl.GetCodereviewServer()
1625
tandriide281ae2016-10-12 06:02:30 -07001626 def GetIssueOwner(self):
1627 """Get owner from codereview, which may differ from this checkout."""
1628 return self._codereview_impl.GetIssueOwner()
1629
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001630 def GetApprovingReviewers(self):
1631 return self._codereview_impl.GetApprovingReviewers()
1632
1633 def GetMostRecentPatchset(self):
1634 return self._codereview_impl.GetMostRecentPatchset()
1635
tandriide281ae2016-10-12 06:02:30 -07001636 def CannotTriggerTryJobReason(self):
1637 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1638 return self._codereview_impl.CannotTriggerTryJobReason()
1639
tandrii8c5a3532016-11-04 07:52:02 -07001640 def GetTryjobProperties(self, patchset=None):
1641 """Returns dictionary of properties to launch tryjob."""
1642 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1643
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001644 def __getattr__(self, attr):
1645 # This is because lots of untested code accesses Rietveld-specific stuff
1646 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001647 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001648 # Note that child method defines __getattr__ as well, and forwards it here,
1649 # because _RietveldChangelistImpl is not cleaned up yet, and given
1650 # deprecation of Rietveld, it should probably be just removed.
1651 # Until that time, avoid infinite recursion by bypassing __getattr__
1652 # of implementation class.
1653 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001654
1655
1656class _ChangelistCodereviewBase(object):
1657 """Abstract base class encapsulating codereview specifics of a changelist."""
1658 def __init__(self, changelist):
1659 self._changelist = changelist # instance of Changelist
1660
1661 def __getattr__(self, attr):
1662 # Forward methods to changelist.
1663 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1664 # _RietveldChangelistImpl to avoid this hack?
1665 return getattr(self._changelist, attr)
1666
1667 def GetStatus(self):
1668 """Apply a rough heuristic to give a simple summary of an issue's review
1669 or CQ status, assuming adherence to a common workflow.
1670
1671 Returns None if no issue for this branch, or specific string keywords.
1672 """
1673 raise NotImplementedError()
1674
1675 def GetCodereviewServer(self):
1676 """Returns server URL without end slash, like "https://codereview.com"."""
1677 raise NotImplementedError()
1678
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001679 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001680 """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
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001815 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001816 issue = self.GetIssue()
1817 assert issue
1818 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001819 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001820 except urllib2.HTTPError as e:
1821 if e.code == 404:
1822 DieWithError(
1823 ('\nWhile fetching the description for issue %d, received a '
1824 '404 (not found)\n'
1825 'error. It is likely that you deleted this '
1826 'issue on the server. If this is the\n'
1827 'case, please run\n\n'
1828 ' git cl issue 0\n\n'
1829 'to clear the association with the deleted issue. Then run '
1830 'this command again.') % issue)
1831 else:
1832 DieWithError(
1833 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1834 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001835 print('Warning: Failed to retrieve CL description due to network '
1836 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001837 return ''
1838
1839 def GetMostRecentPatchset(self):
1840 return self.GetIssueProperties()['patchsets'][-1]
1841
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842 def GetIssueProperties(self):
1843 if self._props is None:
1844 issue = self.GetIssue()
1845 if not issue:
1846 self._props = {}
1847 else:
1848 self._props = self.RpcServer().get_issue_properties(issue, True)
1849 return self._props
1850
tandriie113dfd2016-10-11 10:20:12 -07001851 def CannotTriggerTryJobReason(self):
1852 props = self.GetIssueProperties()
1853 if not props:
1854 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1855 if props.get('closed'):
1856 return 'CL %s is closed' % self.GetIssue()
1857 if props.get('private'):
1858 return 'CL %s is private' % self.GetIssue()
1859 return None
1860
tandrii8c5a3532016-11-04 07:52:02 -07001861 def GetTryjobProperties(self, patchset=None):
1862 """Returns dictionary of properties to launch tryjob."""
1863 project = (self.GetIssueProperties() or {}).get('project')
1864 return {
1865 'issue': self.GetIssue(),
1866 'patch_project': project,
1867 'patch_storage': 'rietveld',
1868 'patchset': patchset or self.GetPatchset(),
1869 'rietveld': self.GetCodereviewServer(),
1870 }
1871
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001872 def GetApprovingReviewers(self):
1873 return get_approving_reviewers(self.GetIssueProperties())
1874
tandriide281ae2016-10-12 06:02:30 -07001875 def GetIssueOwner(self):
1876 return (self.GetIssueProperties() or {}).get('owner_email')
1877
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001878 def AddComment(self, message):
1879 return self.RpcServer().add_comment(self.GetIssue(), message)
1880
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001881 def GetStatus(self):
1882 """Apply a rough heuristic to give a simple summary of an issue's review
1883 or CQ status, assuming adherence to a common workflow.
1884
1885 Returns None if no issue for this branch, or one of the following keywords:
1886 * 'error' - error from review tool (including deleted issues)
1887 * 'unsent' - not sent for review
1888 * 'waiting' - waiting for review
1889 * 'reply' - waiting for owner to reply to review
1890 * 'lgtm' - LGTM from at least one approved reviewer
1891 * 'commit' - in the commit queue
1892 * 'closed' - closed
1893 """
1894 if not self.GetIssue():
1895 return None
1896
1897 try:
1898 props = self.GetIssueProperties()
1899 except urllib2.HTTPError:
1900 return 'error'
1901
1902 if props.get('closed'):
1903 # Issue is closed.
1904 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001905 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001906 # Issue is in the commit queue.
1907 return 'commit'
1908
1909 try:
1910 reviewers = self.GetApprovingReviewers()
1911 except urllib2.HTTPError:
1912 return 'error'
1913
1914 if reviewers:
1915 # Was LGTM'ed.
1916 return 'lgtm'
1917
1918 messages = props.get('messages') or []
1919
tandrii9d2c7a32016-06-22 03:42:45 -07001920 # Skip CQ messages that don't require owner's action.
1921 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1922 if 'Dry run:' in messages[-1]['text']:
1923 messages.pop()
1924 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1925 # This message always follows prior messages from CQ,
1926 # so skip this too.
1927 messages.pop()
1928 else:
1929 # This is probably a CQ messages warranting user attention.
1930 break
1931
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001932 if not messages:
1933 # No message was sent.
1934 return 'unsent'
1935 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001936 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001937 return 'reply'
1938 return 'waiting'
1939
dsansomee2d6fd92016-09-08 00:10:47 -07001940 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001941 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001942
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001944 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001945
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001946 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001947 return self.SetFlags({flag: value})
1948
1949 def SetFlags(self, flags):
1950 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001951 """
phajdan.jr68598232016-08-10 03:28:28 -07001952 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001953 try:
tandrii4b233bd2016-07-06 03:50:29 -07001954 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001955 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001956 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001957 if e.code == 404:
1958 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1959 if e.code == 403:
1960 DieWithError(
1961 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001962 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001963 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001964
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001965 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001966 """Returns an upload.RpcServer() to access this review's rietveld instance.
1967 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001968 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001969 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001970 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001971 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001972 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001974 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001975 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001976 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001977
tandrii5d48c322016-08-18 16:19:37 -07001978 @classmethod
1979 def PatchsetConfigKey(cls):
1980 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001981
tandrii5d48c322016-08-18 16:19:37 -07001982 @classmethod
1983 def CodereviewServerConfigKey(cls):
1984 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001985
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001986 def GetRieveldObjForPresubmit(self):
1987 return self.RpcServer()
1988
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001989 def SetCQState(self, new_state):
1990 props = self.GetIssueProperties()
1991 if props.get('private'):
1992 DieWithError('Cannot set-commit on private issue')
1993
1994 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001995 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001996 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001997 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001998 else:
tandrii4b233bd2016-07-06 03:50:29 -07001999 assert new_state == _CQState.DRY_RUN
2000 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002001
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002002 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2003 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002004 # PatchIssue should never be called with a dirty tree. It is up to the
2005 # caller to check this, but just in case we assert here since the
2006 # consequences of the caller not checking this could be dire.
2007 assert(not git_common.is_dirty_git_tree('apply'))
2008 assert(parsed_issue_arg.valid)
2009 self._changelist.issue = parsed_issue_arg.issue
2010 if parsed_issue_arg.hostname:
2011 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2012
skobes6468b902016-10-24 08:45:10 -07002013 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2014 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2015 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002016 try:
skobes6468b902016-10-24 08:45:10 -07002017 scm_obj.apply_patch(patchset_object)
2018 except Exception as e:
2019 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002020 return 1
2021
2022 # If we had an issue, commit the current state and register the issue.
2023 if not nocommit:
2024 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2025 'patch from issue %(i)s at patchset '
2026 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2027 % {'i': self.GetIssue(), 'p': patchset})])
2028 self.SetIssue(self.GetIssue())
2029 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002030 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002031 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002032 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002033 return 0
2034
2035 @staticmethod
2036 def ParseIssueURL(parsed_url):
2037 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2038 return None
wychen3c1c1722016-08-04 11:46:36 -07002039 # Rietveld patch: https://domain/<number>/#ps<patchset>
2040 match = re.match(r'/(\d+)/$', parsed_url.path)
2041 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2042 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002043 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002044 issue=int(match.group(1)),
2045 patchset=int(match2.group(1)),
2046 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002047 # Typical url: https://domain/<issue_number>[/[other]]
2048 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2049 if match:
skobes6468b902016-10-24 08:45:10 -07002050 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002051 issue=int(match.group(1)),
2052 hostname=parsed_url.netloc)
2053 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2054 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2055 if match:
skobes6468b902016-10-24 08:45:10 -07002056 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002057 issue=int(match.group(1)),
2058 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002059 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002060 return None
2061
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002062 def CMDUploadChange(self, options, args, change):
2063 """Upload the patch to Rietveld."""
2064 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2065 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002066 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2067 if options.emulate_svn_auto_props:
2068 upload_args.append('--emulate_svn_auto_props')
2069
2070 change_desc = None
2071
2072 if options.email is not None:
2073 upload_args.extend(['--email', options.email])
2074
2075 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002076 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002077 upload_args.extend(['--title', options.title])
2078 if options.message:
2079 upload_args.extend(['--message', options.message])
2080 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002081 print('This branch is associated with issue %s. '
2082 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002083 else:
nodirca166002016-06-27 10:59:51 -07002084 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002085 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002086 if options.message:
2087 message = options.message
2088 else:
2089 message = CreateDescriptionFromLog(args)
2090 if options.title:
2091 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002092 change_desc = ChangeDescription(message)
2093 if options.reviewers or options.tbr_owners:
2094 change_desc.update_reviewers(options.reviewers,
2095 options.tbr_owners,
2096 change)
2097 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002098 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002099
2100 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002101 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002102 return 1
2103
2104 upload_args.extend(['--message', change_desc.description])
2105 if change_desc.get_reviewers():
2106 upload_args.append('--reviewers=%s' % ','.join(
2107 change_desc.get_reviewers()))
2108 if options.send_mail:
2109 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002110 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002111 upload_args.append('--send_mail')
2112
2113 # We check this before applying rietveld.private assuming that in
2114 # rietveld.cc only addresses which we can send private CLs to are listed
2115 # if rietveld.private is set, and so we should ignore rietveld.cc only
2116 # when --private is specified explicitly on the command line.
2117 if options.private:
2118 logging.warn('rietveld.cc is ignored since private flag is specified. '
2119 'You need to review and add them manually if necessary.')
2120 cc = self.GetCCListWithoutDefault()
2121 else:
2122 cc = self.GetCCList()
2123 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002124 if change_desc.get_cced():
2125 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002126 if cc:
2127 upload_args.extend(['--cc', cc])
2128
2129 if options.private or settings.GetDefaultPrivateFlag() == "True":
2130 upload_args.append('--private')
2131
2132 upload_args.extend(['--git_similarity', str(options.similarity)])
2133 if not options.find_copies:
2134 upload_args.extend(['--git_no_find_copies'])
2135
2136 # Include the upstream repo's URL in the change -- this is useful for
2137 # projects that have their source spread across multiple repos.
2138 remote_url = self.GetGitBaseUrlFromConfig()
2139 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002140 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2141 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2142 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002143 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002144 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002145 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002146 if target_ref:
2147 upload_args.extend(['--target_ref', target_ref])
2148
2149 # Look for dependent patchsets. See crbug.com/480453 for more details.
2150 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2151 upstream_branch = ShortBranchName(upstream_branch)
2152 if remote is '.':
2153 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002154 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002155 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002156 print()
2157 print('Skipping dependency patchset upload because git config '
2158 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2159 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002160 else:
2161 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002162 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002163 auth_config=auth_config)
2164 branch_cl_issue_url = branch_cl.GetIssueURL()
2165 branch_cl_issue = branch_cl.GetIssue()
2166 branch_cl_patchset = branch_cl.GetPatchset()
2167 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2168 upload_args.extend(
2169 ['--depends_on_patchset', '%s:%s' % (
2170 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002171 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002172 '\n'
2173 'The current branch (%s) is tracking a local branch (%s) with '
2174 'an associated CL.\n'
2175 'Adding %s/#ps%s as a dependency patchset.\n'
2176 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2177 branch_cl_patchset))
2178
2179 project = settings.GetProject()
2180 if project:
2181 upload_args.extend(['--project', project])
2182
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002183 try:
2184 upload_args = ['upload'] + upload_args + args
2185 logging.info('upload.RealMain(%s)', upload_args)
2186 issue, patchset = upload.RealMain(upload_args)
2187 issue = int(issue)
2188 patchset = int(patchset)
2189 except KeyboardInterrupt:
2190 sys.exit(1)
2191 except:
2192 # If we got an exception after the user typed a description for their
2193 # change, back up the description before re-raising.
2194 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002195 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002196 raise
2197
2198 if not self.GetIssue():
2199 self.SetIssue(issue)
2200 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002201 return 0
2202
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002203
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002204class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002205 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002206 # auth_config is Rietveld thing, kept here to preserve interface only.
2207 super(_GerritChangelistImpl, self).__init__(changelist)
2208 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002209 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002210 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002211 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002212 # Map from change number (issue) to its detail cache.
2213 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002214
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002215 if codereview_host is not None:
2216 assert not codereview_host.startswith('https://'), codereview_host
2217 self._gerrit_host = codereview_host
2218 self._gerrit_server = 'https://%s' % codereview_host
2219
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002220 def _GetGerritHost(self):
2221 # Lazy load of configs.
2222 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002223 if self._gerrit_host and '.' not in self._gerrit_host:
2224 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2225 # This happens for internal stuff http://crbug.com/614312.
2226 parsed = urlparse.urlparse(self.GetRemoteUrl())
2227 if parsed.scheme == 'sso':
2228 print('WARNING: using non https URLs for remote is likely broken\n'
2229 ' Your current remote is: %s' % self.GetRemoteUrl())
2230 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2231 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002232 return self._gerrit_host
2233
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002234 def _GetGitHost(self):
2235 """Returns git host to be used when uploading change to Gerrit."""
2236 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2237
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002238 def GetCodereviewServer(self):
2239 if not self._gerrit_server:
2240 # If we're on a branch then get the server potentially associated
2241 # with that branch.
2242 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002243 self._gerrit_server = self._GitGetBranchConfigValue(
2244 self.CodereviewServerConfigKey())
2245 if self._gerrit_server:
2246 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002247 if not self._gerrit_server:
2248 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2249 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002250 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002251 parts[0] = parts[0] + '-review'
2252 self._gerrit_host = '.'.join(parts)
2253 self._gerrit_server = 'https://%s' % self._gerrit_host
2254 return self._gerrit_server
2255
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002256 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002257 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002258 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002259
tandrii5d48c322016-08-18 16:19:37 -07002260 @classmethod
2261 def PatchsetConfigKey(cls):
2262 return 'gerritpatchset'
2263
2264 @classmethod
2265 def CodereviewServerConfigKey(cls):
2266 return 'gerritserver'
2267
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002268 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002269 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002270 if settings.GetGerritSkipEnsureAuthenticated():
2271 # For projects with unusual authentication schemes.
2272 # See http://crbug.com/603378.
2273 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002274 # Lazy-loader to identify Gerrit and Git hosts.
2275 if gerrit_util.GceAuthenticator.is_gce():
2276 return
2277 self.GetCodereviewServer()
2278 git_host = self._GetGitHost()
2279 assert self._gerrit_server and self._gerrit_host
2280 cookie_auth = gerrit_util.CookiesAuthenticator()
2281
2282 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2283 git_auth = cookie_auth.get_auth_header(git_host)
2284 if gerrit_auth and git_auth:
2285 if gerrit_auth == git_auth:
2286 return
2287 print((
2288 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2289 ' Check your %s or %s file for credentials of hosts:\n'
2290 ' %s\n'
2291 ' %s\n'
2292 ' %s') %
2293 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2294 git_host, self._gerrit_host,
2295 cookie_auth.get_new_password_message(git_host)))
2296 if not force:
2297 ask_for_data('If you know what you are doing, press Enter to continue, '
2298 'Ctrl+C to abort.')
2299 return
2300 else:
2301 missing = (
2302 [] if gerrit_auth else [self._gerrit_host] +
2303 [] if git_auth else [git_host])
2304 DieWithError('Credentials for the following hosts are required:\n'
2305 ' %s\n'
2306 'These are read from %s (or legacy %s)\n'
2307 '%s' % (
2308 '\n '.join(missing),
2309 cookie_auth.get_gitcookies_path(),
2310 cookie_auth.get_netrc_path(),
2311 cookie_auth.get_new_password_message(git_host)))
2312
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002313 def _PostUnsetIssueProperties(self):
2314 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002315 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002316
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002317 def GetRieveldObjForPresubmit(self):
2318 class ThisIsNotRietveldIssue(object):
2319 def __nonzero__(self):
2320 # This is a hack to make presubmit_support think that rietveld is not
2321 # defined, yet still ensure that calls directly result in a decent
2322 # exception message below.
2323 return False
2324
2325 def __getattr__(self, attr):
2326 print(
2327 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2328 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2329 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2330 'or use Rietveld for codereview.\n'
2331 'See also http://crbug.com/579160.' % attr)
2332 raise NotImplementedError()
2333 return ThisIsNotRietveldIssue()
2334
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002335 def GetGerritObjForPresubmit(self):
2336 return presubmit_support.GerritAccessor(self._GetGerritHost())
2337
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002338 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002339 """Apply a rough heuristic to give a simple summary of an issue's review
2340 or CQ status, assuming adherence to a common workflow.
2341
2342 Returns None if no issue for this branch, or one of the following keywords:
2343 * 'error' - error from review tool (including deleted issues)
2344 * 'unsent' - no reviewers added
2345 * 'waiting' - waiting for review
2346 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002347 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002348 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002349 * 'commit' - in the commit queue
2350 * 'closed' - abandoned
2351 """
2352 if not self.GetIssue():
2353 return None
2354
2355 try:
2356 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002357 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002358 return 'error'
2359
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002360 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002361 return 'closed'
2362
2363 cq_label = data['labels'].get('Commit-Queue', {})
2364 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002365 votes = cq_label.get('all', [])
2366 highest_vote = 0
2367 for v in votes:
2368 highest_vote = max(highest_vote, v.get('value', 0))
2369 vote_value = str(highest_vote)
2370 if vote_value != '0':
2371 # Add a '+' if the value is not 0 to match the values in the label.
2372 # The cq_label does not have negatives.
2373 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002374 vote_text = cq_label.get('values', {}).get(vote_value, '')
2375 if vote_text.lower() == 'commit':
2376 return 'commit'
2377
2378 lgtm_label = data['labels'].get('Code-Review', {})
2379 if lgtm_label:
2380 if 'rejected' in lgtm_label:
2381 return 'not lgtm'
2382 if 'approved' in lgtm_label:
2383 return 'lgtm'
2384
2385 if not data.get('reviewers', {}).get('REVIEWER', []):
2386 return 'unsent'
2387
2388 messages = data.get('messages', [])
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002389 owner = data['owner'].get('_account_id')
2390 while messages:
2391 last_message_author = messages.pop().get('author', {})
2392 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2393 # Ignore replies from CQ.
2394 continue
Andrii Shyshkalov7afd1642017-01-29 16:07:15 +01002395 if last_message_author.get('_account_id') != owner:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002396 # Some reply from non-owner.
2397 return 'reply'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002398 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002399
2400 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002401 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002402 return data['revisions'][data['current_revision']]['_number']
2403
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002404 def FetchDescription(self, force=False):
2405 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2406 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002407 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002408 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002409
dsansomee2d6fd92016-09-08 00:10:47 -07002410 def UpdateDescriptionRemote(self, description, force=False):
2411 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2412 if not force:
2413 ask_for_data(
2414 'The description cannot be modified while the issue has a pending '
2415 'unpublished edit. Either publish the edit in the Gerrit web UI '
2416 'or delete it.\n\n'
2417 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2418
2419 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2420 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002421 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002422 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002423
2424 def CloseIssue(self):
2425 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2426
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002427 def GetApprovingReviewers(self):
2428 """Returns a list of reviewers approving the change.
2429
2430 Note: not necessarily committers.
2431 """
2432 raise NotImplementedError()
2433
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002434 def SubmitIssue(self, wait_for_merge=True):
2435 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2436 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002437
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002438 def _GetChangeDetail(self, options=None, issue=None,
2439 no_cache=False):
2440 """Returns details of the issue by querying Gerrit and caching results.
2441
2442 If fresh data is needed, set no_cache=True which will clear cache and
2443 thus new data will be fetched from Gerrit.
2444 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002445 options = options or []
2446 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002447 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002448
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002449 # Optimization to avoid multiple RPCs:
2450 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2451 'CURRENT_COMMIT' not in options):
2452 options.append('CURRENT_COMMIT')
2453
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002454 # Normalize issue and options for consistent keys in cache.
2455 issue = str(issue)
2456 options = [o.upper() for o in options]
2457
2458 # Check in cache first unless no_cache is True.
2459 if no_cache:
2460 self._detail_cache.pop(issue, None)
2461 else:
2462 options_set = frozenset(options)
2463 for cached_options_set, data in self._detail_cache.get(issue, []):
2464 # Assumption: data fetched before with extra options is suitable
2465 # for return for a smaller set of options.
2466 # For example, if we cached data for
2467 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2468 # and request is for options=[CURRENT_REVISION],
2469 # THEN we can return prior cached data.
2470 if options_set.issubset(cached_options_set):
2471 return data
2472
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002473 try:
2474 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2475 options, ignore_404=False)
2476 except gerrit_util.GerritError as e:
2477 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002478 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002479 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002480
2481 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002482 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002483
agable32978d92016-11-01 12:55:02 -07002484 def _GetChangeCommit(self, issue=None):
2485 issue = issue or self.GetIssue()
2486 assert issue, 'issue is required to query Gerrit'
2487 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2488 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002489 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002490 return data
2491
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002492 def CMDLand(self, force, bypass_hooks, verbose):
2493 if git_common.is_dirty_git_tree('land'):
2494 return 1
tandriid60367b2016-06-22 05:25:12 -07002495 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2496 if u'Commit-Queue' in detail.get('labels', {}):
2497 if not force:
2498 ask_for_data('\nIt seems this repository has a Commit Queue, '
2499 'which can test and land changes for you. '
2500 'Are you sure you wish to bypass it?\n'
2501 'Press Enter to continue, Ctrl+C to abort.')
2502
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002503 differs = True
tandriic4344b52016-08-29 06:04:54 -07002504 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002505 # Note: git diff outputs nothing if there is no diff.
2506 if not last_upload or RunGit(['diff', last_upload]).strip():
2507 print('WARNING: some changes from local branch haven\'t been uploaded')
2508 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002509 if detail['current_revision'] == last_upload:
2510 differs = False
2511 else:
2512 print('WARNING: local branch contents differ from latest uploaded '
2513 'patchset')
2514 if differs:
2515 if not force:
2516 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002517 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2518 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002519 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2520 elif not bypass_hooks:
2521 hook_results = self.RunHook(
2522 committing=True,
2523 may_prompt=not force,
2524 verbose=verbose,
2525 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2526 if not hook_results.should_continue():
2527 return 1
2528
2529 self.SubmitIssue(wait_for_merge=True)
2530 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002531 links = self._GetChangeCommit().get('web_links', [])
2532 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002533 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002534 print('Landed as %s' % link.get('url'))
2535 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002536 return 0
2537
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002538 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2539 directory):
2540 assert not reject
2541 assert not nocommit
2542 assert not directory
2543 assert parsed_issue_arg.valid
2544
2545 self._changelist.issue = parsed_issue_arg.issue
2546
2547 if parsed_issue_arg.hostname:
2548 self._gerrit_host = parsed_issue_arg.hostname
2549 self._gerrit_server = 'https://%s' % self._gerrit_host
2550
tandriic2405f52016-10-10 08:13:15 -07002551 try:
2552 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002553 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002554 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002555
2556 if not parsed_issue_arg.patchset:
2557 # Use current revision by default.
2558 revision_info = detail['revisions'][detail['current_revision']]
2559 patchset = int(revision_info['_number'])
2560 else:
2561 patchset = parsed_issue_arg.patchset
2562 for revision_info in detail['revisions'].itervalues():
2563 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2564 break
2565 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002566 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002567 (parsed_issue_arg.patchset, self.GetIssue()))
2568
2569 fetch_info = revision_info['fetch']['http']
2570 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2571 RunGit(['cherry-pick', 'FETCH_HEAD'])
2572 self.SetIssue(self.GetIssue())
2573 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002574 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002575 (self.GetIssue(), self.GetPatchset()))
2576 return 0
2577
2578 @staticmethod
2579 def ParseIssueURL(parsed_url):
2580 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2581 return None
2582 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2583 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2584 # Short urls like https://domain/<issue_number> can be used, but don't allow
2585 # specifying the patchset (you'd 404), but we allow that here.
2586 if parsed_url.path == '/':
2587 part = parsed_url.fragment
2588 else:
2589 part = parsed_url.path
2590 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2591 if match:
2592 return _ParsedIssueNumberArgument(
2593 issue=int(match.group(2)),
2594 patchset=int(match.group(4)) if match.group(4) else None,
2595 hostname=parsed_url.netloc)
2596 return None
2597
tandrii16e0b4e2016-06-07 10:34:28 -07002598 def _GerritCommitMsgHookCheck(self, offer_removal):
2599 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2600 if not os.path.exists(hook):
2601 return
2602 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2603 # custom developer made one.
2604 data = gclient_utils.FileRead(hook)
2605 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2606 return
2607 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002608 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002609 'and may interfere with it in subtle ways.\n'
2610 'We recommend you remove the commit-msg hook.')
2611 if offer_removal:
2612 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2613 if reply.lower().startswith('y'):
2614 gclient_utils.rm_file_or_tree(hook)
2615 print('Gerrit commit-msg hook removed.')
2616 else:
2617 print('OK, will keep Gerrit commit-msg hook in place.')
2618
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002619 def CMDUploadChange(self, options, args, change):
2620 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002621 if options.squash and options.no_squash:
2622 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002623
2624 if not options.squash and not options.no_squash:
2625 # Load default for user, repo, squash=true, in this order.
2626 options.squash = settings.GetSquashGerritUploads()
2627 elif options.no_squash:
2628 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002629
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002630 # We assume the remote called "origin" is the one we want.
2631 # It is probably not worthwhile to support different workflows.
2632 gerrit_remote = 'origin'
2633
2634 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002635 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002636
Aaron Gableb56ad332017-01-06 15:24:31 -08002637 # This may be None; default fallback value is determined in logic below.
2638 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002639 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002640
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002641 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002642 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002643 if self.GetIssue():
2644 # Try to get the message from a previous upload.
2645 message = self.GetDescription()
2646 if not message:
2647 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002648 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002649 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002650 if not title:
2651 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2652 title = ask_for_data(
2653 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002654 if title == default_title:
2655 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002656 change_id = self._GetChangeDetail()['change_id']
2657 while True:
2658 footer_change_ids = git_footers.get_footer_change_id(message)
2659 if footer_change_ids == [change_id]:
2660 break
2661 if not footer_change_ids:
2662 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002663 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002664 continue
2665 # There is already a valid footer but with different or several ids.
2666 # Doing this automatically is non-trivial as we don't want to lose
2667 # existing other footers, yet we want to append just 1 desired
2668 # Change-Id. Thus, just create a new footer, but let user verify the
2669 # new description.
2670 message = '%s\n\nChange-Id: %s' % (message, change_id)
2671 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002672 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002673 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002674 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002675 'Please, check the proposed correction to the description, '
2676 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2677 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2678 change_id))
2679 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2680 if not options.force:
2681 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002682 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002683 message = change_desc.description
2684 if not message:
2685 DieWithError("Description is empty. Aborting...")
2686 # Continue the while loop.
2687 # Sanity check of this code - we should end up with proper message
2688 # footer.
2689 assert [change_id] == git_footers.get_footer_change_id(message)
2690 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002691 else: # if not self.GetIssue()
2692 if options.message:
2693 message = options.message
2694 else:
2695 message = CreateDescriptionFromLog(args)
2696 if options.title:
2697 message = options.title + '\n\n' + message
2698 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002699 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002700 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002701 # On first upload, patchset title is always this string, while
2702 # --title flag gets converted to first line of message.
2703 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002704 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002705 if not change_desc.description:
2706 DieWithError("Description is empty. Aborting...")
2707 message = change_desc.description
2708 change_ids = git_footers.get_footer_change_id(message)
2709 if len(change_ids) > 1:
2710 DieWithError('too many Change-Id footers, at most 1 allowed.')
2711 if not change_ids:
2712 # Generate the Change-Id automatically.
2713 message = git_footers.add_footer_change_id(
2714 message, GenerateGerritChangeId(message))
2715 change_desc.set_description(message)
2716 change_ids = git_footers.get_footer_change_id(message)
2717 assert len(change_ids) == 1
2718 change_id = change_ids[0]
2719
2720 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2721 if remote is '.':
2722 # If our upstream branch is local, we base our squashed commit on its
2723 # squashed version.
2724 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2725 # Check the squashed hash of the parent.
2726 parent = RunGit(['config',
2727 'branch.%s.gerritsquashhash' % upstream_branch_name],
2728 error_ok=True).strip()
2729 # Verify that the upstream branch has been uploaded too, otherwise
2730 # Gerrit will create additional CLs when uploading.
2731 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2732 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002733 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002734 '\nUpload upstream branch %s first.\n'
2735 'It is likely that this branch has been rebased since its last '
2736 'upload, so you just need to upload it again.\n'
2737 '(If you uploaded it with --no-squash, then branch dependencies '
2738 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002739 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002740 else:
2741 parent = self.GetCommonAncestorWithUpstream()
2742
2743 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2744 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2745 '-m', message]).strip()
2746 else:
2747 change_desc = ChangeDescription(
2748 options.message or CreateDescriptionFromLog(args))
2749 if not change_desc.description:
2750 DieWithError("Description is empty. Aborting...")
2751
2752 if not git_footers.get_footer_change_id(change_desc.description):
2753 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002754 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2755 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002756 ref_to_push = 'HEAD'
2757 parent = '%s/%s' % (gerrit_remote, branch)
2758 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2759
2760 assert change_desc
2761 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2762 ref_to_push)]).splitlines()
2763 if len(commits) > 1:
2764 print('WARNING: This will upload %d commits. Run the following command '
2765 'to see which commits will be uploaded: ' % len(commits))
2766 print('git log %s..%s' % (parent, ref_to_push))
2767 print('You can also use `git squash-branch` to squash these into a '
2768 'single commit.')
2769 ask_for_data('About to upload; enter to confirm.')
2770
2771 if options.reviewers or options.tbr_owners:
2772 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2773 change)
2774
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002775 # Extra options that can be specified at push time. Doc:
2776 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2777 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002778 if change_desc.get_reviewers(tbr_only=True):
2779 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2780 refspec_opts.append('l=Code-Review+1')
2781
Aaron Gable9b713dd2016-12-14 16:04:21 -08002782 if title:
2783 if not re.match(r'^[\w ]+$', title):
2784 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002785 if not automatic_title:
2786 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gableeb3af712017-02-02 12:42:02 -08002787 'and spaces. You can edit it in the UI. '
2788 'See https://crbug.com/663787.\n'
2789 'Cleaned up title: %s' % title)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002790 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2791 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002792 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002793
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002794 if options.send_mail:
2795 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002796 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002797 refspec_opts.append('notify=ALL')
2798 else:
2799 refspec_opts.append('notify=NONE')
2800
tandrii99a72f22016-08-17 14:33:24 -07002801 reviewers = change_desc.get_reviewers()
2802 if reviewers:
Andrii Shyshkalovaf3a9992017-02-09 14:18:01 +01002803 # TODO(tandrii): remove this horrible hack once (Poly)Gerrit fixes their
2804 # side for real (b/34702620).
2805 def clean_invisible_chars(email):
2806 return email.decode('unicode_escape').encode('ascii', 'ignore')
2807 refspec_opts.extend('r=' + clean_invisible_chars(email).strip()
2808 for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002809
agablec6787972016-09-09 16:13:34 -07002810 if options.private:
2811 refspec_opts.append('draft')
2812
rmistry9eadede2016-09-19 11:22:43 -07002813 if options.topic:
2814 # Documentation on Gerrit topics is here:
2815 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2816 refspec_opts.append('topic=%s' % options.topic)
2817
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002818 refspec_suffix = ''
2819 if refspec_opts:
2820 refspec_suffix = '%' + ','.join(refspec_opts)
2821 assert ' ' not in refspec_suffix, (
2822 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002823 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002824
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002825 try:
2826 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalove9c78ff2017-02-06 15:53:13 +01002827 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002828 print_stdout=True,
2829 # Flush after every line: useful for seeing progress when running as
2830 # recipe.
2831 filter_fn=lambda _: sys.stdout.flush())
2832 except subprocess2.CalledProcessError:
2833 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002834 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002835
2836 if options.squash:
2837 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2838 change_numbers = [m.group(1)
2839 for m in map(regex.match, push_stdout.splitlines())
2840 if m]
2841 if len(change_numbers) != 1:
2842 DieWithError(
2843 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002844 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002845 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002846 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002847
2848 # Add cc's from the CC_LIST and --cc flag (if any).
2849 cc = self.GetCCList().split(',')
2850 if options.cc:
2851 cc.extend(options.cc)
2852 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002853 if change_desc.get_cced():
2854 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002855 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002856 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002857 self._GetGerritHost(), self.GetIssue(), cc,
2858 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002859 return 0
2860
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002861 def _AddChangeIdToCommitMessage(self, options, args):
2862 """Re-commits using the current message, assumes the commit hook is in
2863 place.
2864 """
2865 log_desc = options.message or CreateDescriptionFromLog(args)
2866 git_command = ['commit', '--amend', '-m', log_desc]
2867 RunGit(git_command)
2868 new_log_desc = CreateDescriptionFromLog(args)
2869 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002870 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002871 return new_log_desc
2872 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002873 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002874
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002875 def SetCQState(self, new_state):
2876 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002877 vote_map = {
2878 _CQState.NONE: 0,
2879 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002880 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002881 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002882 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2883 if new_state == _CQState.DRY_RUN:
2884 # Don't spam everybody reviewer/owner.
2885 kwargs['notify'] = 'NONE'
2886 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002887
tandriie113dfd2016-10-11 10:20:12 -07002888 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002889 try:
2890 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002891 except GerritChangeNotExists:
2892 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002893
2894 if data['status'] in ('ABANDONED', 'MERGED'):
2895 return 'CL %s is closed' % self.GetIssue()
2896
2897 def GetTryjobProperties(self, patchset=None):
2898 """Returns dictionary of properties to launch tryjob."""
2899 data = self._GetChangeDetail(['ALL_REVISIONS'])
2900 patchset = int(patchset or self.GetPatchset())
2901 assert patchset
2902 revision_data = None # Pylint wants it to be defined.
2903 for revision_data in data['revisions'].itervalues():
2904 if int(revision_data['_number']) == patchset:
2905 break
2906 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002907 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002908 (patchset, self.GetIssue()))
2909 return {
2910 'patch_issue': self.GetIssue(),
2911 'patch_set': patchset or self.GetPatchset(),
2912 'patch_project': data['project'],
2913 'patch_storage': 'gerrit',
2914 'patch_ref': revision_data['fetch']['http']['ref'],
2915 'patch_repository_url': revision_data['fetch']['http']['url'],
2916 'patch_gerrit_url': self.GetCodereviewServer(),
2917 }
tandriie113dfd2016-10-11 10:20:12 -07002918
tandriide281ae2016-10-12 06:02:30 -07002919 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002920 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002921
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002922
2923_CODEREVIEW_IMPLEMENTATIONS = {
2924 'rietveld': _RietveldChangelistImpl,
2925 'gerrit': _GerritChangelistImpl,
2926}
2927
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002928
iannuccie53c9352016-08-17 14:40:40 -07002929def _add_codereview_issue_select_options(parser, extra=""):
2930 _add_codereview_select_options(parser)
2931
2932 text = ('Operate on this issue number instead of the current branch\'s '
2933 'implicit issue.')
2934 if extra:
2935 text += ' '+extra
2936 parser.add_option('-i', '--issue', type=int, help=text)
2937
2938
2939def _process_codereview_issue_select_options(parser, options):
2940 _process_codereview_select_options(parser, options)
2941 if options.issue is not None and not options.forced_codereview:
2942 parser.error('--issue must be specified with either --rietveld or --gerrit')
2943
2944
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002945def _add_codereview_select_options(parser):
2946 """Appends --gerrit and --rietveld options to force specific codereview."""
2947 parser.codereview_group = optparse.OptionGroup(
2948 parser, 'EXPERIMENTAL! Codereview override options')
2949 parser.add_option_group(parser.codereview_group)
2950 parser.codereview_group.add_option(
2951 '--gerrit', action='store_true',
2952 help='Force the use of Gerrit for codereview')
2953 parser.codereview_group.add_option(
2954 '--rietveld', action='store_true',
2955 help='Force the use of Rietveld for codereview')
2956
2957
2958def _process_codereview_select_options(parser, options):
2959 if options.gerrit and options.rietveld:
2960 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2961 options.forced_codereview = None
2962 if options.gerrit:
2963 options.forced_codereview = 'gerrit'
2964 elif options.rietveld:
2965 options.forced_codereview = 'rietveld'
2966
2967
tandriif9aefb72016-07-01 09:06:51 -07002968def _get_bug_line_values(default_project, bugs):
2969 """Given default_project and comma separated list of bugs, yields bug line
2970 values.
2971
2972 Each bug can be either:
2973 * a number, which is combined with default_project
2974 * string, which is left as is.
2975
2976 This function may produce more than one line, because bugdroid expects one
2977 project per line.
2978
2979 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2980 ['v8:123', 'chromium:789']
2981 """
2982 default_bugs = []
2983 others = []
2984 for bug in bugs.split(','):
2985 bug = bug.strip()
2986 if bug:
2987 try:
2988 default_bugs.append(int(bug))
2989 except ValueError:
2990 others.append(bug)
2991
2992 if default_bugs:
2993 default_bugs = ','.join(map(str, default_bugs))
2994 if default_project:
2995 yield '%s:%s' % (default_project, default_bugs)
2996 else:
2997 yield default_bugs
2998 for other in sorted(others):
2999 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3000 yield other
3001
3002
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003003class ChangeDescription(object):
3004 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003005 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003006 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003007 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003008 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003009
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003010 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003011 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003012
agable@chromium.org42c20792013-09-12 17:34:49 +00003013 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003014 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003015 return '\n'.join(self._description_lines)
3016
3017 def set_description(self, desc):
3018 if isinstance(desc, basestring):
3019 lines = desc.splitlines()
3020 else:
3021 lines = [line.rstrip() for line in desc]
3022 while lines and not lines[0]:
3023 lines.pop(0)
3024 while lines and not lines[-1]:
3025 lines.pop(-1)
3026 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003027
piman@chromium.org336f9122014-09-04 02:16:55 +00003028 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003029 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003030 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003031 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003032 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003033 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003034
agable@chromium.org42c20792013-09-12 17:34:49 +00003035 # Get the set of R= and TBR= lines and remove them from the desciption.
3036 regexp = re.compile(self.R_LINE)
3037 matches = [regexp.match(line) for line in self._description_lines]
3038 new_desc = [l for i, l in enumerate(self._description_lines)
3039 if not matches[i]]
3040 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003041
agable@chromium.org42c20792013-09-12 17:34:49 +00003042 # Construct new unified R= and TBR= lines.
3043 r_names = []
3044 tbr_names = []
3045 for match in matches:
3046 if not match:
3047 continue
3048 people = cleanup_list([match.group(2).strip()])
3049 if match.group(1) == 'TBR':
3050 tbr_names.extend(people)
3051 else:
3052 r_names.extend(people)
3053 for name in r_names:
3054 if name not in reviewers:
3055 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003056 if add_owners_tbr:
3057 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003058 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003059 all_reviewers = set(tbr_names + reviewers)
3060 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3061 all_reviewers)
3062 tbr_names.extend(owners_db.reviewers_for(missing_files,
3063 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003064 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3065 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3066
3067 # Put the new lines in the description where the old first R= line was.
3068 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3069 if 0 <= line_loc < len(self._description_lines):
3070 if new_tbr_line:
3071 self._description_lines.insert(line_loc, new_tbr_line)
3072 if new_r_line:
3073 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003074 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003075 if new_r_line:
3076 self.append_footer(new_r_line)
3077 if new_tbr_line:
3078 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003079
tandriif9aefb72016-07-01 09:06:51 -07003080 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003081 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003082 self.set_description([
3083 '# Enter a description of the change.',
3084 '# This will be displayed on the codereview site.',
3085 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003086 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003087 '--------------------',
3088 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003089
agable@chromium.org42c20792013-09-12 17:34:49 +00003090 regexp = re.compile(self.BUG_LINE)
3091 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003092 prefix = settings.GetBugPrefix()
3093 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3094 for value in values:
3095 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3096 self.append_footer('BUG=%s' % value)
3097
agable@chromium.org42c20792013-09-12 17:34:49 +00003098 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003099 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003100 if not content:
3101 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003102 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003103
3104 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003105 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3106 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003107 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003108 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003109
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003110 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003111 """Adds a footer line to the description.
3112
3113 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3114 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3115 that Gerrit footers are always at the end.
3116 """
3117 parsed_footer_line = git_footers.parse_footer(line)
3118 if parsed_footer_line:
3119 # Line is a gerrit footer in the form: Footer-Key: any value.
3120 # Thus, must be appended observing Gerrit footer rules.
3121 self.set_description(
3122 git_footers.add_footer(self.description,
3123 key=parsed_footer_line[0],
3124 value=parsed_footer_line[1]))
3125 return
3126
3127 if not self._description_lines:
3128 self._description_lines.append(line)
3129 return
3130
3131 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3132 if gerrit_footers:
3133 # git_footers.split_footers ensures that there is an empty line before
3134 # actual (gerrit) footers, if any. We have to keep it that way.
3135 assert top_lines and top_lines[-1] == ''
3136 top_lines, separator = top_lines[:-1], top_lines[-1:]
3137 else:
3138 separator = [] # No need for separator if there are no gerrit_footers.
3139
3140 prev_line = top_lines[-1] if top_lines else ''
3141 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3142 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3143 top_lines.append('')
3144 top_lines.append(line)
3145 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003146
tandrii99a72f22016-08-17 14:33:24 -07003147 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003148 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003149 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003150 reviewers = [match.group(2).strip()
3151 for match in matches
3152 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003153 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003154
bradnelsond975b302016-10-23 12:20:23 -07003155 def get_cced(self):
3156 """Retrieves the list of reviewers."""
3157 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3158 cced = [match.group(2).strip() for match in matches if match]
3159 return cleanup_list(cced)
3160
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003161 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3162 """Updates this commit description given the parent.
3163
3164 This is essentially what Gnumbd used to do.
3165 Consult https://goo.gl/WMmpDe for more details.
3166 """
3167 assert parent_msg # No, orphan branch creation isn't supported.
3168 assert parent_hash
3169 assert dest_ref
3170 parent_footer_map = git_footers.parse_footers(parent_msg)
3171 # This will also happily parse svn-position, which GnumbD is no longer
3172 # supporting. While we'd generate correct footers, the verifier plugin
3173 # installed in Gerrit will block such commit (ie git push below will fail).
3174 parent_position = git_footers.get_position(parent_footer_map)
3175
3176 # Cherry-picks may have last line obscuring their prior footers,
3177 # from git_footers perspective. This is also what Gnumbd did.
3178 cp_line = None
3179 if (self._description_lines and
3180 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3181 cp_line = self._description_lines.pop()
3182
3183 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3184
3185 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3186 # user interference with actual footers we'd insert below.
3187 for i, (k, v) in enumerate(parsed_footers):
3188 if k.startswith('Cr-'):
3189 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3190
3191 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003192 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003193 if parent_position[0] == dest_ref:
3194 # Same branch as parent.
3195 number = int(parent_position[1]) + 1
3196 else:
3197 number = 1 # New branch, and extra lineage.
3198 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3199 int(parent_position[1])))
3200
3201 parsed_footers.append(('Cr-Commit-Position',
3202 '%s@{#%d}' % (dest_ref, number)))
3203 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3204
3205 self._description_lines = top_lines
3206 if cp_line:
3207 self._description_lines.append(cp_line)
3208 if self._description_lines[-1] != '':
3209 self._description_lines.append('') # Ensure footer separator.
3210 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3211
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003212
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003213def get_approving_reviewers(props):
3214 """Retrieves the reviewers that approved a CL from the issue properties with
3215 messages.
3216
3217 Note that the list may contain reviewers that are not committer, thus are not
3218 considered by the CQ.
3219 """
3220 return sorted(
3221 set(
3222 message['sender']
3223 for message in props['messages']
3224 if message['approval'] and message['sender'] in props['reviewers']
3225 )
3226 )
3227
3228
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003229def FindCodereviewSettingsFile(filename='codereview.settings'):
3230 """Finds the given file starting in the cwd and going up.
3231
3232 Only looks up to the top of the repository unless an
3233 'inherit-review-settings-ok' file exists in the root of the repository.
3234 """
3235 inherit_ok_file = 'inherit-review-settings-ok'
3236 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003237 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003238 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3239 root = '/'
3240 while True:
3241 if filename in os.listdir(cwd):
3242 if os.path.isfile(os.path.join(cwd, filename)):
3243 return open(os.path.join(cwd, filename))
3244 if cwd == root:
3245 break
3246 cwd = os.path.dirname(cwd)
3247
3248
3249def LoadCodereviewSettingsFromFile(fileobj):
3250 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003251 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003252
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003253 def SetProperty(name, setting, unset_error_ok=False):
3254 fullname = 'rietveld.' + name
3255 if setting in keyvals:
3256 RunGit(['config', fullname, keyvals[setting]])
3257 else:
3258 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3259
tandrii48df5812016-10-17 03:55:37 -07003260 if not keyvals.get('GERRIT_HOST', False):
3261 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262 # Only server setting is required. Other settings can be absent.
3263 # In that case, we ignore errors raised during option deletion attempt.
3264 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003265 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003266 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3267 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003268 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003269 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3270 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003271 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003272 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3273 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003274
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003275 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003276 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003277
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003278 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003279 RunGit(['config', 'gerrit.squash-uploads',
3280 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003281
tandrii@chromium.org28253532016-04-14 13:46:56 +00003282 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003283 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003284 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003286 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003287 # should be of the form
3288 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3289 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003290 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3291 keyvals['ORIGIN_URL_CONFIG']])
3292
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003293
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003294def urlretrieve(source, destination):
3295 """urllib is broken for SSL connections via a proxy therefore we
3296 can't use urllib.urlretrieve()."""
3297 with open(destination, 'w') as f:
3298 f.write(urllib2.urlopen(source).read())
3299
3300
ukai@chromium.org712d6102013-11-27 00:52:58 +00003301def hasSheBang(fname):
3302 """Checks fname is a #! script."""
3303 with open(fname) as f:
3304 return f.read(2).startswith('#!')
3305
3306
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003307# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3308def DownloadHooks(*args, **kwargs):
3309 pass
3310
3311
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003312def DownloadGerritHook(force):
3313 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003314
3315 Args:
3316 force: True to update hooks. False to install hooks if not present.
3317 """
3318 if not settings.GetIsGerrit():
3319 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003320 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003321 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3322 if not os.access(dst, os.X_OK):
3323 if os.path.exists(dst):
3324 if not force:
3325 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003326 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003327 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003328 if not hasSheBang(dst):
3329 DieWithError('Not a script: %s\n'
3330 'You need to download from\n%s\n'
3331 'into .git/hooks/commit-msg and '
3332 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003333 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3334 except Exception:
3335 if os.path.exists(dst):
3336 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003337 DieWithError('\nFailed to download hooks.\n'
3338 'You need to download from\n%s\n'
3339 'into .git/hooks/commit-msg and '
3340 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003341
3342
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003343def GetRietveldCodereviewSettingsInteractively():
3344 """Prompt the user for settings."""
3345 server = settings.GetDefaultServerUrl(error_ok=True)
3346 prompt = 'Rietveld server (host[:port])'
3347 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3348 newserver = ask_for_data(prompt + ':')
3349 if not server and not newserver:
3350 newserver = DEFAULT_SERVER
3351 if newserver:
3352 newserver = gclient_utils.UpgradeToHttps(newserver)
3353 if newserver != server:
3354 RunGit(['config', 'rietveld.server', newserver])
3355
3356 def SetProperty(initial, caption, name, is_url):
3357 prompt = caption
3358 if initial:
3359 prompt += ' ("x" to clear) [%s]' % initial
3360 new_val = ask_for_data(prompt + ':')
3361 if new_val == 'x':
3362 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3363 elif new_val:
3364 if is_url:
3365 new_val = gclient_utils.UpgradeToHttps(new_val)
3366 if new_val != initial:
3367 RunGit(['config', 'rietveld.' + name, new_val])
3368
3369 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3370 SetProperty(settings.GetDefaultPrivateFlag(),
3371 'Private flag (rietveld only)', 'private', False)
3372 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3373 'tree-status-url', False)
3374 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3375 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3376 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3377 'run-post-upload-hook', False)
3378
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003379
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003380@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003382 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003383
tandrii5d0a0422016-09-14 06:24:35 -07003384 print('WARNING: git cl config works for Rietveld only')
3385 # TODO(tandrii): remove this once we switch to Gerrit.
3386 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003387 parser.add_option('--activate-update', action='store_true',
3388 help='activate auto-updating [rietveld] section in '
3389 '.git/config')
3390 parser.add_option('--deactivate-update', action='store_true',
3391 help='deactivate auto-updating [rietveld] section in '
3392 '.git/config')
3393 options, args = parser.parse_args(args)
3394
3395 if options.deactivate_update:
3396 RunGit(['config', 'rietveld.autoupdate', 'false'])
3397 return
3398
3399 if options.activate_update:
3400 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3401 return
3402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003403 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003404 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003405 return 0
3406
3407 url = args[0]
3408 if not url.endswith('codereview.settings'):
3409 url = os.path.join(url, 'codereview.settings')
3410
3411 # Load code review settings and download hooks (if available).
3412 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3413 return 0
3414
3415
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003416def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003417 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003418 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3419 branch = ShortBranchName(branchref)
3420 _, args = parser.parse_args(args)
3421 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003422 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003423 return RunGit(['config', 'branch.%s.base-url' % branch],
3424 error_ok=False).strip()
3425 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003426 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003427 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3428 error_ok=False).strip()
3429
3430
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003431def color_for_status(status):
3432 """Maps a Changelist status to color, for CMDstatus and other tools."""
3433 return {
3434 'unsent': Fore.RED,
3435 'waiting': Fore.BLUE,
3436 'reply': Fore.YELLOW,
3437 'lgtm': Fore.GREEN,
3438 'commit': Fore.MAGENTA,
3439 'closed': Fore.CYAN,
3440 'error': Fore.WHITE,
3441 }.get(status, Fore.WHITE)
3442
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003443
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003444def get_cl_statuses(changes, fine_grained, max_processes=None):
3445 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003446
3447 If fine_grained is true, this will fetch CL statuses from the server.
3448 Otherwise, simply indicate if there's a matching url for the given branches.
3449
3450 If max_processes is specified, it is used as the maximum number of processes
3451 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3452 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003453
3454 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003455 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003456 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003457 upload.verbosity = 0
3458
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003459 if not changes:
3460 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003461
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003462 if not fine_grained:
3463 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003464 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003465 for cl in changes:
3466 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003467 return
3468
3469 # First, sort out authentication issues.
3470 logging.debug('ensuring credentials exist')
3471 for cl in changes:
3472 cl.EnsureAuthenticated(force=False, refresh=True)
3473
3474 def fetch(cl):
3475 try:
3476 return (cl, cl.GetStatus())
3477 except:
3478 # See http://crbug.com/629863.
3479 logging.exception('failed to fetch status for %s:', cl)
3480 raise
3481
3482 threads_count = len(changes)
3483 if max_processes:
3484 threads_count = max(1, min(threads_count, max_processes))
3485 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3486
3487 pool = ThreadPool(threads_count)
3488 fetched_cls = set()
3489 try:
3490 it = pool.imap_unordered(fetch, changes).__iter__()
3491 while True:
3492 try:
3493 cl, status = it.next(timeout=5)
3494 except multiprocessing.TimeoutError:
3495 break
3496 fetched_cls.add(cl)
3497 yield cl, status
3498 finally:
3499 pool.close()
3500
3501 # Add any branches that failed to fetch.
3502 for cl in set(changes) - fetched_cls:
3503 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003504
rmistry@google.com2dd99862015-06-22 12:22:18 +00003505
3506def upload_branch_deps(cl, args):
3507 """Uploads CLs of local branches that are dependents of the current branch.
3508
3509 If the local branch dependency tree looks like:
3510 test1 -> test2.1 -> test3.1
3511 -> test3.2
3512 -> test2.2 -> test3.3
3513
3514 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3515 run on the dependent branches in this order:
3516 test2.1, test3.1, test3.2, test2.2, test3.3
3517
3518 Note: This function does not rebase your local dependent branches. Use it when
3519 you make a change to the parent branch that will not conflict with its
3520 dependent branches, and you would like their dependencies updated in
3521 Rietveld.
3522 """
3523 if git_common.is_dirty_git_tree('upload-branch-deps'):
3524 return 1
3525
3526 root_branch = cl.GetBranch()
3527 if root_branch is None:
3528 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3529 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003530 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003531 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3532 'patchset dependencies without an uploaded CL.')
3533
3534 branches = RunGit(['for-each-ref',
3535 '--format=%(refname:short) %(upstream:short)',
3536 'refs/heads'])
3537 if not branches:
3538 print('No local branches found.')
3539 return 0
3540
3541 # Create a dictionary of all local branches to the branches that are dependent
3542 # on it.
3543 tracked_to_dependents = collections.defaultdict(list)
3544 for b in branches.splitlines():
3545 tokens = b.split()
3546 if len(tokens) == 2:
3547 branch_name, tracked = tokens
3548 tracked_to_dependents[tracked].append(branch_name)
3549
vapiera7fbd5a2016-06-16 09:17:49 -07003550 print()
3551 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003552 dependents = []
3553 def traverse_dependents_preorder(branch, padding=''):
3554 dependents_to_process = tracked_to_dependents.get(branch, [])
3555 padding += ' '
3556 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003557 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003558 dependents.append(dependent)
3559 traverse_dependents_preorder(dependent, padding)
3560 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003561 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003562
3563 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003564 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003565 return 0
3566
vapiera7fbd5a2016-06-16 09:17:49 -07003567 print('This command will checkout all dependent branches and run '
3568 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003569 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3570
andybons@chromium.org962f9462016-02-03 20:00:42 +00003571 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003572 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003573 args.extend(['-t', 'Updated patchset dependency'])
3574
rmistry@google.com2dd99862015-06-22 12:22:18 +00003575 # Record all dependents that failed to upload.
3576 failures = {}
3577 # Go through all dependents, checkout the branch and upload.
3578 try:
3579 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003580 print()
3581 print('--------------------------------------')
3582 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003583 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003584 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003585 try:
3586 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003587 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003588 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003589 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003590 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003591 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003592 finally:
3593 # Swap back to the original root branch.
3594 RunGit(['checkout', '-q', root_branch])
3595
vapiera7fbd5a2016-06-16 09:17:49 -07003596 print()
3597 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003598 for dependent_branch in dependents:
3599 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003600 print(' %s : %s' % (dependent_branch, upload_status))
3601 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003602
3603 return 0
3604
3605
kmarshall3bff56b2016-06-06 18:31:47 -07003606def CMDarchive(parser, args):
3607 """Archives and deletes branches associated with closed changelists."""
3608 parser.add_option(
3609 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003610 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003611 parser.add_option(
3612 '-f', '--force', action='store_true',
3613 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003614 parser.add_option(
3615 '-d', '--dry-run', action='store_true',
3616 help='Skip the branch tagging and removal steps.')
3617 parser.add_option(
3618 '-t', '--notags', action='store_true',
3619 help='Do not tag archived branches. '
3620 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003621
3622 auth.add_auth_options(parser)
3623 options, args = parser.parse_args(args)
3624 if args:
3625 parser.error('Unsupported args: %s' % ' '.join(args))
3626 auth_config = auth.extract_auth_config_from_options(options)
3627
3628 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3629 if not branches:
3630 return 0
3631
vapiera7fbd5a2016-06-16 09:17:49 -07003632 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003633 changes = [Changelist(branchref=b, auth_config=auth_config)
3634 for b in branches.splitlines()]
3635 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3636 statuses = get_cl_statuses(changes,
3637 fine_grained=True,
3638 max_processes=options.maxjobs)
3639 proposal = [(cl.GetBranch(),
3640 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3641 for cl, status in statuses
3642 if status == 'closed']
3643 proposal.sort()
3644
3645 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003646 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003647 return 0
3648
3649 current_branch = GetCurrentBranch()
3650
vapiera7fbd5a2016-06-16 09:17:49 -07003651 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003652 if options.notags:
3653 for next_item in proposal:
3654 print(' ' + next_item[0])
3655 else:
3656 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3657 for next_item in proposal:
3658 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003659
kmarshall9249e012016-08-23 12:02:16 -07003660 # Quit now on precondition failure or if instructed by the user, either
3661 # via an interactive prompt or by command line flags.
3662 if options.dry_run:
3663 print('\nNo changes were made (dry run).\n')
3664 return 0
3665 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003666 print('You are currently on a branch \'%s\' which is associated with a '
3667 'closed codereview issue, so archive cannot proceed. Please '
3668 'checkout another branch and run this command again.' %
3669 current_branch)
3670 return 1
kmarshall9249e012016-08-23 12:02:16 -07003671 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003672 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3673 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003674 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003675 return 1
3676
3677 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003678 if not options.notags:
3679 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003680 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003681
vapiera7fbd5a2016-06-16 09:17:49 -07003682 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003683
3684 return 0
3685
3686
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003687def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003688 """Show status of changelists.
3689
3690 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003691 - Red not sent for review or broken
3692 - Blue waiting for review
3693 - Yellow waiting for you to reply to review
3694 - Green LGTM'ed
3695 - Magenta in the commit queue
3696 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003697
3698 Also see 'git cl comments'.
3699 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003700 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003701 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003702 parser.add_option('-f', '--fast', action='store_true',
3703 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003704 parser.add_option(
3705 '-j', '--maxjobs', action='store', type=int,
3706 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003707
3708 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003709 _add_codereview_issue_select_options(
3710 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003711 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003712 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003713 if args:
3714 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003715 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003716
iannuccie53c9352016-08-17 14:40:40 -07003717 if options.issue is not None and not options.field:
3718 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003719
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003720 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003721 cl = Changelist(auth_config=auth_config, issue=options.issue,
3722 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003723 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003724 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003725 elif options.field == 'id':
3726 issueid = cl.GetIssue()
3727 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003728 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003729 elif options.field == 'patch':
3730 patchset = cl.GetPatchset()
3731 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003732 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003733 elif options.field == 'status':
3734 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003735 elif options.field == 'url':
3736 url = cl.GetIssueURL()
3737 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003738 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003739 return 0
3740
3741 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3742 if not branches:
3743 print('No local branch found.')
3744 return 0
3745
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003746 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003747 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003748 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003749 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003750 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003751 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003752 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003753
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003754 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003755 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3756 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3757 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003758 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003759 c, status = output.next()
3760 branch_statuses[c.GetBranch()] = status
3761 status = branch_statuses.pop(branch)
3762 url = cl.GetIssueURL()
3763 if url and (not status or status == 'error'):
3764 # The issue probably doesn't exist anymore.
3765 url += ' (broken)'
3766
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003767 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003768 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003769 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003770 color = ''
3771 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003772 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003773 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003774 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003775 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003776
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003777
3778 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003779 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003780 print('Current branch: %s' % branch)
3781 for cl in changes:
3782 if cl.GetBranch() == branch:
3783 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003784 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003785 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003786 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003787 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003788 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003789 print('Issue description:')
3790 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003791 return 0
3792
3793
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003794def colorize_CMDstatus_doc():
3795 """To be called once in main() to add colors to git cl status help."""
3796 colors = [i for i in dir(Fore) if i[0].isupper()]
3797
3798 def colorize_line(line):
3799 for color in colors:
3800 if color in line.upper():
3801 # Extract whitespaces first and the leading '-'.
3802 indent = len(line) - len(line.lstrip(' ')) + 1
3803 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3804 return line
3805
3806 lines = CMDstatus.__doc__.splitlines()
3807 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3808
3809
phajdan.jre328cf92016-08-22 04:12:17 -07003810def write_json(path, contents):
3811 with open(path, 'w') as f:
3812 json.dump(contents, f)
3813
3814
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003815@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003816def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003817 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003818
3819 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003820 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003821 parser.add_option('-r', '--reverse', action='store_true',
3822 help='Lookup the branch(es) for the specified issues. If '
3823 'no issues are specified, all branches with mapped '
3824 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003825 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003826 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003827 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003828 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003829
dnj@chromium.org406c4402015-03-03 17:22:28 +00003830 if options.reverse:
3831 branches = RunGit(['for-each-ref', 'refs/heads',
3832 '--format=%(refname:short)']).splitlines()
3833
3834 # Reverse issue lookup.
3835 issue_branch_map = {}
3836 for branch in branches:
3837 cl = Changelist(branchref=branch)
3838 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3839 if not args:
3840 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003841 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003842 for issue in args:
3843 if not issue:
3844 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003845 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003846 print('Branch for issue number %s: %s' % (
3847 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003848 if options.json:
3849 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003850 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003851 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003852 if len(args) > 0:
3853 try:
3854 issue = int(args[0])
3855 except ValueError:
3856 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003857 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003858 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003859 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003860 if options.json:
3861 write_json(options.json, {
3862 'issue': cl.GetIssue(),
3863 'issue_url': cl.GetIssueURL(),
3864 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003865 return 0
3866
3867
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003868def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003869 """Shows or posts review comments for any changelist."""
3870 parser.add_option('-a', '--add-comment', dest='comment',
3871 help='comment to add to an issue')
3872 parser.add_option('-i', dest='issue',
3873 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003874 parser.add_option('-j', '--json-file',
3875 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003876 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003877 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003878 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003879
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003880 issue = None
3881 if options.issue:
3882 try:
3883 issue = int(options.issue)
3884 except ValueError:
3885 DieWithError('A review issue id is expected to be a number')
3886
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003887 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003888
3889 if options.comment:
3890 cl.AddComment(options.comment)
3891 return 0
3892
3893 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003894 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003895 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003896 summary.append({
3897 'date': message['date'],
3898 'lgtm': False,
3899 'message': message['text'],
3900 'not_lgtm': False,
3901 'sender': message['sender'],
3902 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003903 if message['disapproval']:
3904 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003905 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003906 elif message['approval']:
3907 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003908 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003909 elif message['sender'] == data['owner_email']:
3910 color = Fore.MAGENTA
3911 else:
3912 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003913 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003914 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003915 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003916 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003917 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003918 if options.json_file:
3919 with open(options.json_file, 'wb') as f:
3920 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003921 return 0
3922
3923
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003924@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003925def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003926 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003927 parser.add_option('-d', '--display', action='store_true',
3928 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003929 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003930 help='New description to set for this issue (- for stdin, '
3931 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003932 parser.add_option('-f', '--force', action='store_true',
3933 help='Delete any unpublished Gerrit edits for this issue '
3934 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003935
3936 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003937 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003938 options, args = parser.parse_args(args)
3939 _process_codereview_select_options(parser, options)
3940
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003941 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003942 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003943 target_issue_arg = ParseIssueNumberArgument(args[0])
3944 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003945 parser.print_help()
3946 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003947
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003948 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003949
martiniss6eda05f2016-06-30 10:18:35 -07003950 kwargs = {
3951 'auth_config': auth_config,
3952 'codereview': options.forced_codereview,
3953 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003954 if target_issue_arg:
3955 kwargs['issue'] = target_issue_arg.issue
3956 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07003957
3958 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003959
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003960 if not cl.GetIssue():
3961 DieWithError('This branch has no associated changelist.')
3962 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003963
smut@google.com34fb6b12015-07-13 20:03:26 +00003964 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003965 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003966 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003967
3968 if options.new_description:
3969 text = options.new_description
3970 if text == '-':
3971 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003972 elif text == '+':
3973 base_branch = cl.GetCommonAncestorWithUpstream()
3974 change = cl.GetChange(base_branch, None, local_description=True)
3975 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003976
3977 description.set_description(text)
3978 else:
3979 description.prompt()
3980
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003981 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003982 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003983 return 0
3984
3985
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003986def CreateDescriptionFromLog(args):
3987 """Pulls out the commit log to use as a base for the CL description."""
3988 log_args = []
3989 if len(args) == 1 and not args[0].endswith('.'):
3990 log_args = [args[0] + '..']
3991 elif len(args) == 1 and args[0].endswith('...'):
3992 log_args = [args[0][:-1]]
3993 elif len(args) == 2:
3994 log_args = [args[0] + '..' + args[1]]
3995 else:
3996 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003997 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998
3999
thestig@chromium.org44202a22014-03-11 19:22:18 +00004000def CMDlint(parser, args):
4001 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004002 parser.add_option('--filter', action='append', metavar='-x,+y',
4003 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004004 auth.add_auth_options(parser)
4005 options, args = parser.parse_args(args)
4006 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004007
4008 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004009 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004010 try:
4011 import cpplint
4012 import cpplint_chromium
4013 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004014 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004015 return 1
4016
4017 # Change the current working directory before calling lint so that it
4018 # shows the correct base.
4019 previous_cwd = os.getcwd()
4020 os.chdir(settings.GetRoot())
4021 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004022 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004023 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4024 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004025 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004026 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004027 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004028
4029 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004030 command = args + files
4031 if options.filter:
4032 command = ['--filter=' + ','.join(options.filter)] + command
4033 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004034
4035 white_regex = re.compile(settings.GetLintRegex())
4036 black_regex = re.compile(settings.GetLintIgnoreRegex())
4037 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4038 for filename in filenames:
4039 if white_regex.match(filename):
4040 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004041 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004042 else:
4043 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4044 extra_check_functions)
4045 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004046 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004047 finally:
4048 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004049 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004050 if cpplint._cpplint_state.error_count != 0:
4051 return 1
4052 return 0
4053
4054
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004055def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004056 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004057 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004058 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004059 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004060 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004061 auth.add_auth_options(parser)
4062 options, args = parser.parse_args(args)
4063 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004064
sbc@chromium.org71437c02015-04-09 19:29:40 +00004065 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004066 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004067 return 1
4068
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004069 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004070 if args:
4071 base_branch = args[0]
4072 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004073 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004074 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004075
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004076 cl.RunHook(
4077 committing=not options.upload,
4078 may_prompt=False,
4079 verbose=options.verbose,
4080 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004081 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004082
4083
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004084def GenerateGerritChangeId(message):
4085 """Returns Ixxxxxx...xxx change id.
4086
4087 Works the same way as
4088 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4089 but can be called on demand on all platforms.
4090
4091 The basic idea is to generate git hash of a state of the tree, original commit
4092 message, author/committer info and timestamps.
4093 """
4094 lines = []
4095 tree_hash = RunGitSilent(['write-tree'])
4096 lines.append('tree %s' % tree_hash.strip())
4097 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4098 if code == 0:
4099 lines.append('parent %s' % parent.strip())
4100 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4101 lines.append('author %s' % author.strip())
4102 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4103 lines.append('committer %s' % committer.strip())
4104 lines.append('')
4105 # Note: Gerrit's commit-hook actually cleans message of some lines and
4106 # whitespace. This code is not doing this, but it clearly won't decrease
4107 # entropy.
4108 lines.append(message)
4109 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4110 stdin='\n'.join(lines))
4111 return 'I%s' % change_hash.strip()
4112
4113
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004114def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004115 """Computes the remote branch ref to use for the CL.
4116
4117 Args:
4118 remote (str): The git remote for the CL.
4119 remote_branch (str): The git remote branch for the CL.
4120 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004121 """
4122 if not (remote and remote_branch):
4123 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004124
wittman@chromium.org455dc922015-01-26 20:15:50 +00004125 if target_branch:
4126 # Cannonicalize branch references to the equivalent local full symbolic
4127 # refs, which are then translated into the remote full symbolic refs
4128 # below.
4129 if '/' not in target_branch:
4130 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4131 else:
4132 prefix_replacements = (
4133 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4134 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4135 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4136 )
4137 match = None
4138 for regex, replacement in prefix_replacements:
4139 match = re.search(regex, target_branch)
4140 if match:
4141 remote_branch = target_branch.replace(match.group(0), replacement)
4142 break
4143 if not match:
4144 # This is a branch path but not one we recognize; use as-is.
4145 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004146 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4147 # Handle the refs that need to land in different refs.
4148 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004149
wittman@chromium.org455dc922015-01-26 20:15:50 +00004150 # Create the true path to the remote branch.
4151 # Does the following translation:
4152 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4153 # * refs/remotes/origin/master -> refs/heads/master
4154 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4155 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4156 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4157 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4158 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4159 'refs/heads/')
4160 elif remote_branch.startswith('refs/remotes/branch-heads'):
4161 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004162
wittman@chromium.org455dc922015-01-26 20:15:50 +00004163 return remote_branch
4164
4165
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004166def cleanup_list(l):
4167 """Fixes a list so that comma separated items are put as individual items.
4168
4169 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4170 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4171 """
4172 items = sum((i.split(',') for i in l), [])
4173 stripped_items = (i.strip() for i in items)
4174 return sorted(filter(None, stripped_items))
4175
4176
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004177@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004178def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004179 """Uploads the current changelist to codereview.
4180
4181 Can skip dependency patchset uploads for a branch by running:
4182 git config branch.branch_name.skip-deps-uploads True
4183 To unset run:
4184 git config --unset branch.branch_name.skip-deps-uploads
4185 Can also set the above globally by using the --global flag.
4186 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004187 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4188 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004189 parser.add_option('--bypass-watchlists', action='store_true',
4190 dest='bypass_watchlists',
4191 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004192 parser.add_option('-f', action='store_true', dest='force',
4193 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004194 parser.add_option('--message', '-m', dest='message',
4195 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004196 parser.add_option('-b', '--bug',
4197 help='pre-populate the bug number(s) for this issue. '
4198 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004199 parser.add_option('--message-file', dest='message_file',
4200 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004201 parser.add_option('--title', '-t', dest='title',
4202 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004203 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004204 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004205 help='reviewer email addresses')
4206 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004207 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004208 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004209 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004210 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004211 parser.add_option('--emulate_svn_auto_props',
4212 '--emulate-svn-auto-props',
4213 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004214 dest="emulate_svn_auto_props",
4215 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004216 parser.add_option('-c', '--use-commit-queue', action='store_true',
4217 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004218 parser.add_option('--private', action='store_true',
4219 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004220 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004221 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004222 metavar='TARGET',
4223 help='Apply CL to remote ref TARGET. ' +
4224 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004225 parser.add_option('--squash', action='store_true',
4226 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004227 parser.add_option('--no-squash', action='store_true',
4228 help='Don\'t squash multiple commits into one ' +
4229 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004230 parser.add_option('--topic', default=None,
4231 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004232 parser.add_option('--email', default=None,
4233 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004234 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4235 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004236 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4237 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004238 help='Send the patchset to do a CQ dry run right after '
4239 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004240 parser.add_option('--dependencies', action='store_true',
4241 help='Uploads CLs of all the local branches that depend on '
4242 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004243
rmistry@google.com2dd99862015-06-22 12:22:18 +00004244 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004245 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004246 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004247 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004248 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004249 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004250 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004251
sbc@chromium.org71437c02015-04-09 19:29:40 +00004252 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004253 return 1
4254
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004255 options.reviewers = cleanup_list(options.reviewers)
4256 options.cc = cleanup_list(options.cc)
4257
tandriib80458a2016-06-23 12:20:07 -07004258 if options.message_file:
4259 if options.message:
4260 parser.error('only one of --message and --message-file allowed.')
4261 options.message = gclient_utils.FileRead(options.message_file)
4262 options.message_file = None
4263
tandrii4d0545a2016-07-06 03:56:49 -07004264 if options.cq_dry_run and options.use_commit_queue:
4265 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4266
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004267 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4268 settings.GetIsGerrit()
4269
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004270 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004271 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004272
4273
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004274@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004275def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004276 """DEPRECATED: Used to commit the current changelist via git-svn."""
4277 message = ('git-cl no longer supports committing to SVN repositories via '
4278 'git-svn. You probably want to use `git cl land` instead.')
4279 print(message)
4280 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004281
4282
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004283@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004284def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004285 """Commits the current changelist via git.
4286
4287 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4288 upstream and closes the issue automatically and atomically.
4289
4290 Otherwise (in case of Rietveld):
4291 Squashes branch into a single commit.
4292 Updates commit message with metadata (e.g. pointer to review).
4293 Pushes the code upstream.
4294 Updates review and closes.
4295 """
4296 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4297 help='bypass upload presubmit hook')
4298 parser.add_option('-m', dest='message',
4299 help="override review description")
4300 parser.add_option('-f', action='store_true', dest='force',
4301 help="force yes to questions (don't prompt)")
4302 parser.add_option('-c', dest='contributor',
4303 help="external contributor for patch (appended to " +
4304 "description and used as author for git). Should be " +
4305 "formatted as 'First Last <email@example.com>'")
4306 add_git_similarity(parser)
4307 auth.add_auth_options(parser)
4308 (options, args) = parser.parse_args(args)
4309 auth_config = auth.extract_auth_config_from_options(options)
4310
4311 cl = Changelist(auth_config=auth_config)
4312
4313 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4314 if cl.IsGerrit():
4315 if options.message:
4316 # This could be implemented, but it requires sending a new patch to
4317 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4318 # Besides, Gerrit has the ability to change the commit message on submit
4319 # automatically, thus there is no need to support this option (so far?).
4320 parser.error('-m MESSAGE option is not supported for Gerrit.')
4321 if options.contributor:
4322 parser.error(
4323 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4324 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4325 'the contributor\'s "name <email>". If you can\'t upload such a '
4326 'commit for review, contact your repository admin and request'
4327 '"Forge-Author" permission.')
4328 if not cl.GetIssue():
4329 DieWithError('You must upload the change first to Gerrit.\n'
4330 ' If you would rather have `git cl land` upload '
4331 'automatically for you, see http://crbug.com/642759')
4332 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4333 options.verbose)
4334
4335 current = cl.GetBranch()
4336 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4337 if remote == '.':
4338 print()
4339 print('Attempting to push branch %r into another local branch!' % current)
4340 print()
4341 print('Either reparent this branch on top of origin/master:')
4342 print(' git reparent-branch --root')
4343 print()
4344 print('OR run `git rebase-update` if you think the parent branch is ')
4345 print('already committed.')
4346 print()
4347 print(' Current parent: %r' % upstream_branch)
4348 return 1
4349
4350 if not args:
4351 # Default to merging against our best guess of the upstream branch.
4352 args = [cl.GetUpstreamBranch()]
4353
4354 if options.contributor:
4355 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4356 print("Please provide contibutor as 'First Last <email@example.com>'")
4357 return 1
4358
4359 base_branch = args[0]
4360
4361 if git_common.is_dirty_git_tree('land'):
4362 return 1
4363
4364 # This rev-list syntax means "show all commits not in my branch that
4365 # are in base_branch".
4366 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4367 base_branch]).splitlines()
4368 if upstream_commits:
4369 print('Base branch "%s" has %d commits '
4370 'not in this branch.' % (base_branch, len(upstream_commits)))
4371 print('Run "git merge %s" before attempting to land.' % base_branch)
4372 return 1
4373
4374 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4375 if not options.bypass_hooks:
4376 author = None
4377 if options.contributor:
4378 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4379 hook_results = cl.RunHook(
4380 committing=True,
4381 may_prompt=not options.force,
4382 verbose=options.verbose,
4383 change=cl.GetChange(merge_base, author))
4384 if not hook_results.should_continue():
4385 return 1
4386
4387 # Check the tree status if the tree status URL is set.
4388 status = GetTreeStatus()
4389 if 'closed' == status:
4390 print('The tree is closed. Please wait for it to reopen. Use '
4391 '"git cl land --bypass-hooks" to commit on a closed tree.')
4392 return 1
4393 elif 'unknown' == status:
4394 print('Unable to determine tree status. Please verify manually and '
4395 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4396 return 1
4397
4398 change_desc = ChangeDescription(options.message)
4399 if not change_desc.description and cl.GetIssue():
4400 change_desc = ChangeDescription(cl.GetDescription())
4401
4402 if not change_desc.description:
4403 if not cl.GetIssue() and options.bypass_hooks:
4404 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4405 else:
4406 print('No description set.')
4407 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4408 return 1
4409
4410 # Keep a separate copy for the commit message, because the commit message
4411 # contains the link to the Rietveld issue, while the Rietveld message contains
4412 # the commit viewvc url.
4413 if cl.GetIssue():
4414 change_desc.update_reviewers(cl.GetApprovingReviewers())
4415
4416 commit_desc = ChangeDescription(change_desc.description)
4417 if cl.GetIssue():
4418 # Xcode won't linkify this URL unless there is a non-whitespace character
4419 # after it. Add a period on a new line to circumvent this. Also add a space
4420 # before the period to make sure that Gitiles continues to correctly resolve
4421 # the URL.
4422 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4423 if options.contributor:
4424 commit_desc.append_footer('Patch from %s.' % options.contributor)
4425
4426 print('Description:')
4427 print(commit_desc.description)
4428
4429 branches = [merge_base, cl.GetBranchRef()]
4430 if not options.force:
4431 print_stats(options.similarity, options.find_copies, branches)
4432
4433 # We want to squash all this branch's commits into one commit with the proper
4434 # description. We do this by doing a "reset --soft" to the base branch (which
4435 # keeps the working copy the same), then landing that.
4436 MERGE_BRANCH = 'git-cl-commit'
4437 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4438 # Delete the branches if they exist.
4439 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4440 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4441 result = RunGitWithCode(showref_cmd)
4442 if result[0] == 0:
4443 RunGit(['branch', '-D', branch])
4444
4445 # We might be in a directory that's present in this branch but not in the
4446 # trunk. Move up to the top of the tree so that git commands that expect a
4447 # valid CWD won't fail after we check out the merge branch.
4448 rel_base_path = settings.GetRelativeRoot()
4449 if rel_base_path:
4450 os.chdir(rel_base_path)
4451
4452 # Stuff our change into the merge branch.
4453 # We wrap in a try...finally block so if anything goes wrong,
4454 # we clean up the branches.
4455 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004456 revision = None
4457 try:
4458 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4459 RunGit(['reset', '--soft', merge_base])
4460 if options.contributor:
4461 RunGit(
4462 [
4463 'commit', '--author', options.contributor,
4464 '-m', commit_desc.description,
4465 ])
4466 else:
4467 RunGit(['commit', '-m', commit_desc.description])
4468
4469 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4470 mirror = settings.GetGitMirror(remote)
4471 if mirror:
4472 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004473 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004474 else:
4475 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004476 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004477 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4478
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004479 if git_numberer_enabled:
4480 # TODO(tandrii): maybe do autorebase + retry on failure
4481 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004482 logging.debug('Adding git number footers')
4483 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4484 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4485 branch)
4486 # Ensure timestamps are monotonically increasing.
4487 timestamp = max(1 + _get_committer_timestamp(merge_base),
4488 _get_committer_timestamp('HEAD'))
4489 _git_amend_head(commit_desc.description, timestamp)
4490 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004491
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004492 retcode, output = RunGitWithCode(
4493 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004494 if retcode == 0:
4495 revision = RunGit(['rev-parse', 'HEAD']).strip()
4496 logging.debug(output)
4497 except: # pylint: disable=bare-except
4498 if _IS_BEING_TESTED:
4499 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4500 + '-' * 30 + '8<' + '-' * 30)
4501 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4502 raise
4503 finally:
4504 # And then swap back to the original branch and clean up.
4505 RunGit(['checkout', '-q', cl.GetBranch()])
4506 RunGit(['branch', '-D', MERGE_BRANCH])
4507
4508 if not revision:
4509 print('Failed to push. If this persists, please file a bug.')
4510 return 1
4511
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004512 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004513 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004514 if viewvc_url and revision:
4515 change_desc.append_footer(
4516 'Committed: %s%s' % (viewvc_url, revision))
4517 elif revision:
4518 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004519 print('Closing issue '
4520 '(you may be prompted for your codereview password)...')
4521 cl.UpdateDescription(change_desc.description)
4522 cl.CloseIssue()
4523 props = cl.GetIssueProperties()
4524 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004525 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4526 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004527 if options.bypass_hooks:
4528 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4529 else:
4530 comment += ' (presubmit successful).'
4531 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4532
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004533 if os.path.isfile(POSTUPSTREAM_HOOK):
4534 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4535
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004536 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537
4538
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004539@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004540def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004541 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004542 parser.add_option('-b', dest='newbranch',
4543 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004544 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004545 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004546 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4547 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004548 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004549 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004550 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004551 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004552 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004553 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004554
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004555
4556 group = optparse.OptionGroup(
4557 parser,
4558 'Options for continuing work on the current issue uploaded from a '
4559 'different clone (e.g. different machine). Must be used independently '
4560 'from the other options. No issue number should be specified, and the '
4561 'branch must have an issue number associated with it')
4562 group.add_option('--reapply', action='store_true', dest='reapply',
4563 help='Reset the branch and reapply the issue.\n'
4564 'CAUTION: This will undo any local changes in this '
4565 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004566
4567 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004568 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004569 parser.add_option_group(group)
4570
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004571 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004572 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004573 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004574 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004575 auth_config = auth.extract_auth_config_from_options(options)
4576
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004577
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004578 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004579 if options.newbranch:
4580 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004581 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004582 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004583
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004584 cl = Changelist(auth_config=auth_config,
4585 codereview=options.forced_codereview)
4586 if not cl.GetIssue():
4587 parser.error('current branch must have an associated issue')
4588
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004589 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004590 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004591 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004592
4593 RunGit(['reset', '--hard', upstream])
4594 if options.pull:
4595 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004596
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004597 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4598 options.directory)
4599
4600 if len(args) != 1 or not args[0]:
4601 parser.error('Must specify issue number or url')
4602
4603 # We don't want uncommitted changes mixed up with the patch.
4604 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004605 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004606
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004607 if options.newbranch:
4608 if options.force:
4609 RunGit(['branch', '-D', options.newbranch],
4610 stderr=subprocess2.PIPE, error_ok=True)
4611 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004612 elif not GetCurrentBranch():
4613 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004614
4615 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4616
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004617 if cl.IsGerrit():
4618 if options.reject:
4619 parser.error('--reject is not supported with Gerrit codereview.')
4620 if options.nocommit:
4621 parser.error('--nocommit is not supported with Gerrit codereview.')
4622 if options.directory:
4623 parser.error('--directory is not supported with Gerrit codereview.')
4624
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004625 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004626 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004627
4628
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004629def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004630 """Fetches the tree status and returns either 'open', 'closed',
4631 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004632 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004633 if url:
4634 status = urllib2.urlopen(url).read().lower()
4635 if status.find('closed') != -1 or status == '0':
4636 return 'closed'
4637 elif status.find('open') != -1 or status == '1':
4638 return 'open'
4639 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004640 return 'unset'
4641
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004642
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004643def GetTreeStatusReason():
4644 """Fetches the tree status from a json url and returns the message
4645 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004646 url = settings.GetTreeStatusUrl()
4647 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004648 connection = urllib2.urlopen(json_url)
4649 status = json.loads(connection.read())
4650 connection.close()
4651 return status['message']
4652
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004653
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004654def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004655 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004656 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004657 status = GetTreeStatus()
4658 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004659 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004660 return 2
4661
vapiera7fbd5a2016-06-16 09:17:49 -07004662 print('The tree is %s' % status)
4663 print()
4664 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004665 if status != 'open':
4666 return 1
4667 return 0
4668
4669
maruel@chromium.org15192402012-09-06 12:38:29 +00004670def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004671 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004672 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004673 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004674 '-b', '--bot', action='append',
4675 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4676 'times to specify multiple builders. ex: '
4677 '"-b win_rel -b win_layout". See '
4678 'the try server waterfall for the builders name and the tests '
4679 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004680 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004681 '-B', '--bucket', default='',
4682 help=('Buildbucket bucket to send the try requests.'))
4683 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004684 '-m', '--master', default='',
4685 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004686 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004687 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004688 help='Revision to use for the try job; default: the revision will '
4689 'be determined by the try recipe that builder runs, which usually '
4690 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004691 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004692 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004693 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004694 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004695 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004696 '--project',
4697 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004698 'in recipe to determine to which repository or directory to '
4699 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004700 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004701 '-p', '--property', dest='properties', action='append', default=[],
4702 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004703 'key2=value2 etc. The value will be treated as '
4704 'json if decodable, or as string otherwise. '
4705 'NOTE: using this may make your try job not usable for CQ, '
4706 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004707 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004708 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4709 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004710 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004711 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004712 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004713 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004714
machenbach@chromium.org45453142015-09-15 08:45:22 +00004715 # Make sure that all properties are prop=value pairs.
4716 bad_params = [x for x in options.properties if '=' not in x]
4717 if bad_params:
4718 parser.error('Got properties with missing "=": %s' % bad_params)
4719
maruel@chromium.org15192402012-09-06 12:38:29 +00004720 if args:
4721 parser.error('Unknown arguments: %s' % args)
4722
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004723 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004724 if not cl.GetIssue():
4725 parser.error('Need to upload first')
4726
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004727 if cl.IsGerrit():
4728 # HACK: warm up Gerrit change detail cache to save on RPCs.
4729 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4730
tandriie113dfd2016-10-11 10:20:12 -07004731 error_message = cl.CannotTriggerTryJobReason()
4732 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004733 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004734
borenet6c0efe62016-10-19 08:13:29 -07004735 if options.bucket and options.master:
4736 parser.error('Only one of --bucket and --master may be used.')
4737
qyearsley1fdfcb62016-10-24 13:22:03 -07004738 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004739
qyearsleydd49f942016-10-28 11:57:22 -07004740 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4741 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004742 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004743 if options.verbose:
4744 print('git cl try with no bots now defaults to CQ Dry Run.')
4745 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004746
borenet6c0efe62016-10-19 08:13:29 -07004747 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004748 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004749 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004750 'of bot requires an initial job from a parent (usually a builder). '
4751 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004752 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004753 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004754
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004755 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004756 # TODO(tandrii): Checking local patchset against remote patchset is only
4757 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4758 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004759 print('Warning: Codereview server has newer patchsets (%s) than most '
4760 'recent upload from local checkout (%s). Did a previous upload '
4761 'fail?\n'
4762 'By default, git cl try uses the latest patchset from '
4763 'codereview, continuing to use patchset %s.\n' %
4764 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004765
tandrii568043b2016-10-11 07:49:18 -07004766 try:
borenet6c0efe62016-10-19 08:13:29 -07004767 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4768 patchset)
tandrii568043b2016-10-11 07:49:18 -07004769 except BuildbucketResponseException as ex:
4770 print('ERROR: %s' % ex)
4771 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004772 return 0
4773
4774
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004775def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004776 """Prints info about try jobs associated with current CL."""
4777 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004778 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004779 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004780 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004781 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004782 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004783 '--color', action='store_true', default=setup_color.IS_TTY,
4784 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004785 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004786 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4787 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004788 group.add_option(
4789 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004790 parser.add_option_group(group)
4791 auth.add_auth_options(parser)
4792 options, args = parser.parse_args(args)
4793 if args:
4794 parser.error('Unrecognized args: %s' % ' '.join(args))
4795
4796 auth_config = auth.extract_auth_config_from_options(options)
4797 cl = Changelist(auth_config=auth_config)
4798 if not cl.GetIssue():
4799 parser.error('Need to upload first')
4800
tandrii221ab252016-10-06 08:12:04 -07004801 patchset = options.patchset
4802 if not patchset:
4803 patchset = cl.GetMostRecentPatchset()
4804 if not patchset:
4805 parser.error('Codereview doesn\'t know about issue %s. '
4806 'No access to issue or wrong issue number?\n'
4807 'Either upload first, or pass --patchset explicitely' %
4808 cl.GetIssue())
4809
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004810 # TODO(tandrii): Checking local patchset against remote patchset is only
4811 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4812 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004813 print('Warning: Codereview server has newer patchsets (%s) than most '
4814 'recent upload from local checkout (%s). Did a previous upload '
4815 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004816 'By default, git cl try-results uses the latest patchset from '
4817 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004818 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004819 try:
tandrii221ab252016-10-06 08:12:04 -07004820 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004821 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004822 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004823 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004824 if options.json:
4825 write_try_results_json(options.json, jobs)
4826 else:
4827 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004828 return 0
4829
4830
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004831@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004832def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004833 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004834 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004835 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004836 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004837
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004838 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004839 if args:
4840 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004841 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004842 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004843 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004844 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004845
4846 # Clear configured merge-base, if there is one.
4847 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004848 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004849 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004850 return 0
4851
4852
thestig@chromium.org00858c82013-12-02 23:08:03 +00004853def CMDweb(parser, args):
4854 """Opens the current CL in the web browser."""
4855 _, args = parser.parse_args(args)
4856 if args:
4857 parser.error('Unrecognized args: %s' % ' '.join(args))
4858
4859 issue_url = Changelist().GetIssueURL()
4860 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004861 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004862 return 1
4863
4864 webbrowser.open(issue_url)
4865 return 0
4866
4867
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004868def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004869 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004870 parser.add_option('-d', '--dry-run', action='store_true',
4871 help='trigger in dry run mode')
4872 parser.add_option('-c', '--clear', action='store_true',
4873 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004874 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004875 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004876 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004877 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004878 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004879 if args:
4880 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004881 if options.dry_run and options.clear:
4882 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4883
iannuccie53c9352016-08-17 14:40:40 -07004884 cl = Changelist(auth_config=auth_config, issue=options.issue,
4885 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004886 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004887 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004888 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004889 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004890 state = _CQState.DRY_RUN
4891 else:
4892 state = _CQState.COMMIT
4893 if not cl.GetIssue():
4894 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004895 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004896 return 0
4897
4898
groby@chromium.org411034a2013-02-26 15:12:01 +00004899def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004900 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004901 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004902 auth.add_auth_options(parser)
4903 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004904 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004905 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004906 if args:
4907 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004908 cl = Changelist(auth_config=auth_config, issue=options.issue,
4909 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004910 # Ensure there actually is an issue to close.
4911 cl.GetDescription()
4912 cl.CloseIssue()
4913 return 0
4914
4915
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004916def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004917 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004918 parser.add_option(
4919 '--stat',
4920 action='store_true',
4921 dest='stat',
4922 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004923 auth.add_auth_options(parser)
4924 options, args = parser.parse_args(args)
4925 auth_config = auth.extract_auth_config_from_options(options)
4926 if args:
4927 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004928
4929 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004930 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004931 # Staged changes would be committed along with the patch from last
4932 # upload, hence counted toward the "last upload" side in the final
4933 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004934 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004935 return 1
4936
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004937 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004938 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004939 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004940 if not issue:
4941 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004942 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004943 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004944
4945 # Create a new branch based on the merge-base
4946 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004947 # Clear cached branch in cl object, to avoid overwriting original CL branch
4948 # properties.
4949 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004950 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004951 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004952 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004953 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004954 return rtn
4955
wychen@chromium.org06928532015-02-03 02:11:29 +00004956 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004957 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004958 cmd = ['git', 'diff']
4959 if options.stat:
4960 cmd.append('--stat')
4961 cmd.extend([TMP_BRANCH, branch, '--'])
4962 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004963 finally:
4964 RunGit(['checkout', '-q', branch])
4965 RunGit(['branch', '-D', TMP_BRANCH])
4966
4967 return 0
4968
4969
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004970def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004971 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004972 parser.add_option(
4973 '--no-color',
4974 action='store_true',
4975 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004976 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004977 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004978 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004979
4980 author = RunGit(['config', 'user.email']).strip() or None
4981
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004982 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004983
4984 if args:
4985 if len(args) > 1:
4986 parser.error('Unknown args')
4987 base_branch = args[0]
4988 else:
4989 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004990 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004991
4992 change = cl.GetChange(base_branch, None)
4993 return owners_finder.OwnersFinder(
4994 [f.LocalPath() for f in
4995 cl.GetChange(base_branch, None).AffectedFiles()],
4996 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004997 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004998 disable_color=options.no_color).run()
4999
5000
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005001def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005002 """Generates a diff command."""
5003 # Generate diff for the current branch's changes.
5004 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005005 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005006
5007 if args:
5008 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005009 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005010 diff_cmd.append(arg)
5011 else:
5012 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005013
5014 return diff_cmd
5015
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005016
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005017def MatchingFileType(file_name, extensions):
5018 """Returns true if the file name ends with one of the given extensions."""
5019 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005020
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005021
enne@chromium.org555cfe42014-01-29 18:21:39 +00005022@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005023def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005024 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005025 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005026 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005027 parser.add_option('--full', action='store_true',
5028 help='Reformat the full content of all touched files')
5029 parser.add_option('--dry-run', action='store_true',
5030 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005031 parser.add_option('--python', action='store_true',
5032 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005033 parser.add_option('--js', action='store_true',
5034 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005035 parser.add_option('--diff', action='store_true',
5036 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005037 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005038
Daniel Chengc55eecf2016-12-30 03:11:02 -08005039 # Normalize any remaining args against the current path, so paths relative to
5040 # the current directory are still resolved as expected.
5041 args = [os.path.join(os.getcwd(), arg) for arg in args]
5042
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005043 # git diff generates paths against the root of the repository. Change
5044 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005045 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005046 if rel_base_path:
5047 os.chdir(rel_base_path)
5048
digit@chromium.org29e47272013-05-17 17:01:46 +00005049 # Grab the merge-base commit, i.e. the upstream commit of the current
5050 # branch when it was created or the last time it was rebased. This is
5051 # to cover the case where the user may have called "git fetch origin",
5052 # moving the origin branch to a newer commit, but hasn't rebased yet.
5053 upstream_commit = None
5054 cl = Changelist()
5055 upstream_branch = cl.GetUpstreamBranch()
5056 if upstream_branch:
5057 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5058 upstream_commit = upstream_commit.strip()
5059
5060 if not upstream_commit:
5061 DieWithError('Could not find base commit for this branch. '
5062 'Are you in detached state?')
5063
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005064 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5065 diff_output = RunGit(changed_files_cmd)
5066 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005067 # Filter out files deleted by this CL
5068 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005069
Christopher Lamc5ba6922017-01-24 11:19:14 +11005070 if opts.js:
5071 CLANG_EXTS.append('.js')
5072
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005073 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5074 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5075 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005076 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005077
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005078 top_dir = os.path.normpath(
5079 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5080
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005081 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5082 # formatted. This is used to block during the presubmit.
5083 return_value = 0
5084
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005085 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005086 # Locate the clang-format binary in the checkout
5087 try:
5088 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005089 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005090 DieWithError(e)
5091
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005092 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005093 cmd = [clang_format_tool]
5094 if not opts.dry_run and not opts.diff:
5095 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005096 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005097 if opts.diff:
5098 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005099 else:
5100 env = os.environ.copy()
5101 env['PATH'] = str(os.path.dirname(clang_format_tool))
5102 try:
5103 script = clang_format.FindClangFormatScriptInChromiumTree(
5104 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005105 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005106 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005107
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005108 cmd = [sys.executable, script, '-p0']
5109 if not opts.dry_run and not opts.diff:
5110 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005111
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005112 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5113 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005114
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005115 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5116 if opts.diff:
5117 sys.stdout.write(stdout)
5118 if opts.dry_run and len(stdout) > 0:
5119 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005120
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005121 # Similar code to above, but using yapf on .py files rather than clang-format
5122 # on C/C++ files
5123 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005124 yapf_tool = gclient_utils.FindExecutable('yapf')
5125 if yapf_tool is None:
5126 DieWithError('yapf not found in PATH')
5127
5128 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005129 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005130 cmd = [yapf_tool]
5131 if not opts.dry_run and not opts.diff:
5132 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005133 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005134 if opts.diff:
5135 sys.stdout.write(stdout)
5136 else:
5137 # TODO(sbc): yapf --lines mode still has some issues.
5138 # https://github.com/google/yapf/issues/154
5139 DieWithError('--python currently only works with --full')
5140
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005141 # Dart's formatter does not have the nice property of only operating on
5142 # modified chunks, so hard code full.
5143 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005144 try:
5145 command = [dart_format.FindDartFmtToolInChromiumTree()]
5146 if not opts.dry_run and not opts.diff:
5147 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005148 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005149
ppi@chromium.org6593d932016-03-03 15:41:15 +00005150 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005151 if opts.dry_run and stdout:
5152 return_value = 2
5153 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005154 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5155 'found in this checkout. Files in other languages are still '
5156 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005157
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005158 # Format GN build files. Always run on full build files for canonical form.
5159 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005160 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005161 if opts.dry_run or opts.diff:
5162 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005163 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005164 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5165 shell=sys.platform == 'win32',
5166 cwd=top_dir)
5167 if opts.dry_run and gn_ret == 2:
5168 return_value = 2 # Not formatted.
5169 elif opts.diff and gn_ret == 2:
5170 # TODO this should compute and print the actual diff.
5171 print("This change has GN build file diff for " + gn_diff_file)
5172 elif gn_ret != 0:
5173 # For non-dry run cases (and non-2 return values for dry-run), a
5174 # nonzero error code indicates a failure, probably because the file
5175 # doesn't parse.
5176 DieWithError("gn format failed on " + gn_diff_file +
5177 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005178
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005179 metrics_xml_files = [
5180 'tools/metrics/actions/actions.xml',
5181 'tools/metrics/histograms/histograms.xml',
5182 'tools/metrics/rappor/rappor.xml']
5183 for xml_file in metrics_xml_files:
5184 if xml_file in diff_files:
5185 tool_dir = top_dir + '/' + os.path.dirname(xml_file)
5186 cmd = [tool_dir + '/pretty_print.py', '--non-interactive']
5187 if opts.dry_run or opts.diff:
5188 cmd.append('--diff')
5189 stdout = RunCommand(cmd, cwd=top_dir)
5190 if opts.diff:
5191 sys.stdout.write(stdout)
5192 if opts.dry_run and stdout:
5193 return_value = 2 # Not formatted.
5194
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005195 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005196
5197
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005198@subcommand.usage('<codereview url or issue id>')
5199def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005200 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005201 _, args = parser.parse_args(args)
5202
5203 if len(args) != 1:
5204 parser.print_help()
5205 return 1
5206
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005207 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005208 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005209 parser.print_help()
5210 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005211 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005212
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005213 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005214 output = RunGit(['config', '--local', '--get-regexp',
5215 r'branch\..*\.%s' % issueprefix],
5216 error_ok=True)
5217 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005218 if issue == target_issue:
5219 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005220
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005221 branches = []
5222 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005223 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005224 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005225 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005226 return 1
5227 if len(branches) == 1:
5228 RunGit(['checkout', branches[0]])
5229 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005230 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005231 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005232 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005233 which = raw_input('Choose by index: ')
5234 try:
5235 RunGit(['checkout', branches[int(which)]])
5236 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005237 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005238 return 1
5239
5240 return 0
5241
5242
maruel@chromium.org29404b52014-09-08 22:58:00 +00005243def CMDlol(parser, args):
5244 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005245 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005246 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5247 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5248 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005249 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005250 return 0
5251
5252
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005253class OptionParser(optparse.OptionParser):
5254 """Creates the option parse and add --verbose support."""
5255 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005256 optparse.OptionParser.__init__(
5257 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005258 self.add_option(
5259 '-v', '--verbose', action='count', default=0,
5260 help='Use 2 times for more debugging info')
5261
5262 def parse_args(self, args=None, values=None):
5263 options, args = optparse.OptionParser.parse_args(self, args, values)
5264 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005265 logging.basicConfig(
5266 level=levels[min(options.verbose, len(levels) - 1)],
5267 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5268 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005269 return options, args
5270
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005272def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005273 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005274 print('\nYour python version %s is unsupported, please upgrade.\n' %
5275 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005276 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005277
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005278 # Reload settings.
5279 global settings
5280 settings = Settings()
5281
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005282 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005283 dispatcher = subcommand.CommandDispatcher(__name__)
5284 try:
5285 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005286 except auth.AuthenticationError as e:
5287 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005288 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005289 if e.code != 500:
5290 raise
5291 DieWithError(
5292 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5293 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005294 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005295
5296
5297if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005298 # These affect sys.stdout so do it outside of main() to simplify mocks in
5299 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005300 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005301 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005302 try:
5303 sys.exit(main(sys.argv[1:]))
5304 except KeyboardInterrupt:
5305 sys.stderr.write('interrupted\n')
5306 sys.exit(1)