blob: 47d5d01213758862e2579f26748ac5c4fb84c5d1 [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
Mark Mentovai57c47212017-03-09 11:14:09 -0500814 def GetBugLineFormat(self):
815 # rietveld.bug-line-format should have a %s where the list of bugs should
816 # go. This is a bit of a quirk, because normal people will always want the
817 # bug list to go right after a prefix like BUG= or Bug:. The %s format
818 # approach is used strictly because there isn't a great way to carry the
819 # desired space after Bug: all the way from codereview.settings to here
820 # without treating : specially or inventing a quoting scheme.
821 bug_line_format = self._GetRietveldConfig('bug-line-format', error_ok=True)
822 if not bug_line_format:
823 # TODO(tandrii): change this to 'Bug: %s' to be a proper Gerrit footer.
824 bug_line_format = 'BUG=%s'
825 return bug_line_format
826
rmistry@google.com90752582014-01-14 21:04:50 +0000827 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000828 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000829
rmistry@google.com78948ed2015-07-08 23:09:57 +0000830 def GetIsSkipDependencyUpload(self, branch_name):
831 """Returns true if specified branch should skip dep uploads."""
832 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
833 error_ok=True)
834
rmistry@google.com5626a922015-02-26 14:03:30 +0000835 def GetRunPostUploadHook(self):
836 run_post_upload_hook = self._GetRietveldConfig(
837 'run-post-upload-hook', error_ok=True)
838 return run_post_upload_hook == "True"
839
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000840 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000841 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000842
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000843 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000844 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000845
ukai@chromium.orge8077812012-02-03 03:41:46 +0000846 def GetIsGerrit(self):
847 """Return true if this repo is assosiated with gerrit code review system."""
848 if self.is_gerrit is None:
849 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
850 return self.is_gerrit
851
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000852 def GetSquashGerritUploads(self):
853 """Return true if uploads to Gerrit should be squashed by default."""
854 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700855 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
856 if self.squash_gerrit_uploads is None:
857 # Default is squash now (http://crbug.com/611892#c23).
858 self.squash_gerrit_uploads = not (
859 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
860 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000861 return self.squash_gerrit_uploads
862
tandriia60502f2016-06-20 02:01:53 -0700863 def GetSquashGerritUploadsOverride(self):
864 """Return True or False if codereview.settings should be overridden.
865
866 Returns None if no override has been defined.
867 """
868 # See also http://crbug.com/611892#c23
869 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
870 error_ok=True).strip()
871 if result == 'true':
872 return True
873 if result == 'false':
874 return False
875 return None
876
tandrii@chromium.org28253532016-04-14 13:46:56 +0000877 def GetGerritSkipEnsureAuthenticated(self):
878 """Return True if EnsureAuthenticated should not be done for Gerrit
879 uploads."""
880 if self.gerrit_skip_ensure_authenticated is None:
881 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000882 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000883 error_ok=True).strip() == 'true')
884 return self.gerrit_skip_ensure_authenticated
885
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000886 def GetGitEditor(self):
887 """Return the editor specified in the git config, or None if none is."""
888 if self.git_editor is None:
889 self.git_editor = self._GetConfig('core.editor', error_ok=True)
890 return self.git_editor or None
891
thestig@chromium.org44202a22014-03-11 19:22:18 +0000892 def GetLintRegex(self):
893 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
894 DEFAULT_LINT_REGEX)
895
896 def GetLintIgnoreRegex(self):
897 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
898 DEFAULT_LINT_IGNORE_REGEX)
899
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000900 def GetProject(self):
901 if not self.project:
902 self.project = self._GetRietveldConfig('project', error_ok=True)
903 return self.project
904
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000905 def _GetRietveldConfig(self, param, **kwargs):
906 return self._GetConfig('rietveld.' + param, **kwargs)
907
rmistry@google.com78948ed2015-07-08 23:09:57 +0000908 def _GetBranchConfig(self, branch_name, param, **kwargs):
909 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
910
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000911 def _GetConfig(self, param, **kwargs):
912 self.LazyUpdateIfNeeded()
913 return RunGit(['config', param], **kwargs).strip()
914
915
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100916@contextlib.contextmanager
917def _get_gerrit_project_config_file(remote_url):
918 """Context manager to fetch and store Gerrit's project.config from
919 refs/meta/config branch and store it in temp file.
920
921 Provides a temporary filename or None if there was error.
922 """
923 error, _ = RunGitWithCode([
924 'fetch', remote_url,
925 '+refs/meta/config:refs/git_cl/meta/config'])
926 if error:
927 # Ref doesn't exist or isn't accessible to current user.
928 print('WARNING: failed to fetch project config for %s: %s' %
929 (remote_url, error))
930 yield None
931 return
932
933 error, project_config_data = RunGitWithCode(
934 ['show', 'refs/git_cl/meta/config:project.config'])
935 if error:
936 print('WARNING: project.config file not found')
937 yield None
938 return
939
940 with gclient_utils.temporary_directory() as tempdir:
941 project_config_file = os.path.join(tempdir, 'project.config')
942 gclient_utils.FileWrite(project_config_file, project_config_data)
943 yield project_config_file
944
945
946def _is_git_numberer_enabled(remote_url, remote_ref):
947 """Returns True if Git Numberer is enabled on this ref."""
948 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100949 KNOWN_PROJECTS_WHITELIST = [
950 'chromium/src',
951 'external/webrtc',
952 'v8/v8',
953 ]
954
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100955 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
956 url_parts = urlparse.urlparse(remote_url)
957 project_name = url_parts.path.lstrip('/').rstrip('git./')
958 for known in KNOWN_PROJECTS_WHITELIST:
959 if project_name.endswith(known):
960 break
961 else:
962 # Early exit to avoid extra fetches for repos that aren't using Git
963 # Numberer.
964 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100965
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100966 with _get_gerrit_project_config_file(remote_url) as project_config_file:
967 if project_config_file is None:
968 # Failed to fetch project.config, which shouldn't happen on open source
969 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100970 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100971 def get_opts(x):
972 code, out = RunGitWithCode(
973 ['config', '-f', project_config_file, '--get-all',
974 'plugin.git-numberer.validate-%s-refglob' % x])
975 if code == 0:
976 return out.strip().splitlines()
977 return []
978 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100979
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100980 logging.info('validator config enabled %s disabled %s refglobs for '
981 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000982
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100983 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100984 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100985 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100986 return True
987 return False
988
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100989 if match_refglobs(disabled):
990 return False
991 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100992
993
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000994def ShortBranchName(branch):
995 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000996 return branch.replace('refs/heads/', '', 1)
997
998
999def GetCurrentBranchRef():
1000 """Returns branch ref (e.g., refs/heads/master) or None."""
1001 return RunGit(['symbolic-ref', 'HEAD'],
1002 stderr=subprocess2.VOID, error_ok=True).strip() or None
1003
1004
1005def GetCurrentBranch():
1006 """Returns current branch or None.
1007
1008 For refs/heads/* branches, returns just last part. For others, full ref.
1009 """
1010 branchref = GetCurrentBranchRef()
1011 if branchref:
1012 return ShortBranchName(branchref)
1013 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014
1015
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001016class _CQState(object):
1017 """Enum for states of CL with respect to Commit Queue."""
1018 NONE = 'none'
1019 DRY_RUN = 'dry_run'
1020 COMMIT = 'commit'
1021
1022 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1023
1024
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001025class _ParsedIssueNumberArgument(object):
1026 def __init__(self, issue=None, patchset=None, hostname=None):
1027 self.issue = issue
1028 self.patchset = patchset
1029 self.hostname = hostname
1030
1031 @property
1032 def valid(self):
1033 return self.issue is not None
1034
1035
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001036def ParseIssueNumberArgument(arg):
1037 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1038 fail_result = _ParsedIssueNumberArgument()
1039
1040 if arg.isdigit():
1041 return _ParsedIssueNumberArgument(issue=int(arg))
1042 if not arg.startswith('http'):
1043 return fail_result
1044 url = gclient_utils.UpgradeToHttps(arg)
1045 try:
1046 parsed_url = urlparse.urlparse(url)
1047 except ValueError:
1048 return fail_result
1049 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1050 tmp = cls.ParseIssueURL(parsed_url)
1051 if tmp is not None:
1052 return tmp
1053 return fail_result
1054
1055
Aaron Gablea45ee112016-11-22 15:14:38 -08001056class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001057 def __init__(self, issue, url):
1058 self.issue = issue
1059 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001060 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001061
1062 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001063 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001064 self.issue, self.url)
1065
1066
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001067class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001068 """Changelist works with one changelist in local branch.
1069
1070 Supports two codereview backends: Rietveld or Gerrit, selected at object
1071 creation.
1072
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001073 Notes:
1074 * Not safe for concurrent multi-{thread,process} use.
1075 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001076 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001077 """
1078
1079 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1080 """Create a new ChangeList instance.
1081
1082 If issue is given, the codereview must be given too.
1083
1084 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1085 Otherwise, it's decided based on current configuration of the local branch,
1086 with default being 'rietveld' for backwards compatibility.
1087 See _load_codereview_impl for more details.
1088
1089 **kwargs will be passed directly to codereview implementation.
1090 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001092 global settings
1093 if not settings:
1094 # Happens when git_cl.py is used as a utility library.
1095 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001096
1097 if issue:
1098 assert codereview, 'codereview must be known, if issue is known'
1099
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 self.branchref = branchref
1101 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001102 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103 self.branch = ShortBranchName(self.branchref)
1104 else:
1105 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001107 self.lookedup_issue = False
1108 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109 self.has_description = False
1110 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001111 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001112 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001113 self.cc = None
1114 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001115 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001116
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001117 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001118 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001119 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001120 assert self._codereview_impl
1121 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001122
1123 def _load_codereview_impl(self, codereview=None, **kwargs):
1124 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001125 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1126 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1127 self._codereview = codereview
1128 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001129 return
1130
1131 # Automatic selection based on issue number set for a current branch.
1132 # Rietveld takes precedence over Gerrit.
1133 assert not self.issue
1134 # Whether we find issue or not, we are doing the lookup.
1135 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001136 if self.GetBranch():
1137 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1138 issue = _git_get_branch_config_value(
1139 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1140 if issue:
1141 self._codereview = codereview
1142 self._codereview_impl = cls(self, **kwargs)
1143 self.issue = int(issue)
1144 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001145
1146 # No issue is set for this branch, so decide based on repo-wide settings.
1147 return self._load_codereview_impl(
1148 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1149 **kwargs)
1150
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001151 def IsGerrit(self):
1152 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153
1154 def GetCCList(self):
1155 """Return the users cc'd on this CL.
1156
agable92bec4f2016-08-24 09:27:27 -07001157 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001158 """
1159 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001160 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001161 more_cc = ','.join(self.watchers)
1162 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1163 return self.cc
1164
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001165 def GetCCListWithoutDefault(self):
1166 """Return the users cc'd on this CL excluding default ones."""
1167 if self.cc is None:
1168 self.cc = ','.join(self.watchers)
1169 return self.cc
1170
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001171 def SetWatchers(self, watchers):
1172 """Set the list of email addresses that should be cc'd based on the changed
1173 files in this CL.
1174 """
1175 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176
1177 def GetBranch(self):
1178 """Returns the short branch name, e.g. 'master'."""
1179 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001180 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001181 if not branchref:
1182 return None
1183 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 self.branch = ShortBranchName(self.branchref)
1185 return self.branch
1186
1187 def GetBranchRef(self):
1188 """Returns the full branch name, e.g. 'refs/heads/master'."""
1189 self.GetBranch() # Poke the lazy loader.
1190 return self.branchref
1191
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001192 def ClearBranch(self):
1193 """Clears cached branch data of this object."""
1194 self.branch = self.branchref = None
1195
tandrii5d48c322016-08-18 16:19:37 -07001196 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1197 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1198 kwargs['branch'] = self.GetBranch()
1199 return _git_get_branch_config_value(key, default, **kwargs)
1200
1201 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1202 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1203 assert self.GetBranch(), (
1204 'this CL must have an associated branch to %sset %s%s' %
1205 ('un' if value is None else '',
1206 key,
1207 '' if value is None else ' to %r' % value))
1208 kwargs['branch'] = self.GetBranch()
1209 return _git_set_branch_config_value(key, value, **kwargs)
1210
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001211 @staticmethod
1212 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001213 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214 e.g. 'origin', 'refs/heads/master'
1215 """
1216 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001217 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1218
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001219 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001220 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001222 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1223 error_ok=True).strip()
1224 if upstream_branch:
1225 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001227 # Else, try to guess the origin remote.
1228 remote_branches = RunGit(['branch', '-r']).split()
1229 if 'origin/master' in remote_branches:
1230 # Fall back on origin/master if it exits.
1231 remote = 'origin'
1232 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001234 DieWithError(
1235 'Unable to determine default branch to diff against.\n'
1236 'Either pass complete "git diff"-style arguments, like\n'
1237 ' git cl upload origin/master\n'
1238 'or verify this branch is set up to track another \n'
1239 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240
1241 return remote, upstream_branch
1242
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001243 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001244 upstream_branch = self.GetUpstreamBranch()
1245 if not BranchExists(upstream_branch):
1246 DieWithError('The upstream for the current branch (%s) does not exist '
1247 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001248 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001249 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001250
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 def GetUpstreamBranch(self):
1252 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001253 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001255 upstream_branch = upstream_branch.replace('refs/heads/',
1256 'refs/remotes/%s/' % remote)
1257 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1258 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 self.upstream_branch = upstream_branch
1260 return self.upstream_branch
1261
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001262 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001263 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001264 remote, branch = None, self.GetBranch()
1265 seen_branches = set()
1266 while branch not in seen_branches:
1267 seen_branches.add(branch)
1268 remote, branch = self.FetchUpstreamTuple(branch)
1269 branch = ShortBranchName(branch)
1270 if remote != '.' or branch.startswith('refs/remotes'):
1271 break
1272 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001273 remotes = RunGit(['remote'], error_ok=True).split()
1274 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001275 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001276 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001277 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001278 logging.warn('Could not determine which remote this change is '
1279 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001280 else:
1281 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001282 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001283 branch = 'HEAD'
1284 if branch.startswith('refs/remotes'):
1285 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001286 elif branch.startswith('refs/branch-heads/'):
1287 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001288 else:
1289 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001290 return self._remote
1291
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001292 def GitSanityChecks(self, upstream_git_obj):
1293 """Checks git repo status and ensures diff is from local commits."""
1294
sbc@chromium.org79706062015-01-14 21:18:12 +00001295 if upstream_git_obj is None:
1296 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001297 print('ERROR: unable to determine current branch (detached HEAD?)',
1298 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001299 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001300 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001301 return False
1302
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001303 # Verify the commit we're diffing against is in our current branch.
1304 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1305 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1306 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001307 print('ERROR: %s is not in the current branch. You may need to rebase '
1308 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001309 return False
1310
1311 # List the commits inside the diff, and verify they are all local.
1312 commits_in_diff = RunGit(
1313 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1314 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1315 remote_branch = remote_branch.strip()
1316 if code != 0:
1317 _, remote_branch = self.GetRemoteBranch()
1318
1319 commits_in_remote = RunGit(
1320 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1321
1322 common_commits = set(commits_in_diff) & set(commits_in_remote)
1323 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001324 print('ERROR: Your diff contains %d commits already in %s.\n'
1325 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1326 'the diff. If you are using a custom git flow, you can override'
1327 ' the reference used for this check with "git config '
1328 'gitcl.remotebranch <git-ref>".' % (
1329 len(common_commits), remote_branch, upstream_git_obj),
1330 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001331 return False
1332 return True
1333
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001334 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001335 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001336
1337 Returns None if it is not set.
1338 """
tandrii5d48c322016-08-18 16:19:37 -07001339 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001340
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341 def GetRemoteUrl(self):
1342 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1343
1344 Returns None if there is no remote.
1345 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001346 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001347 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1348
1349 # If URL is pointing to a local directory, it is probably a git cache.
1350 if os.path.isdir(url):
1351 url = RunGit(['config', 'remote.%s.url' % remote],
1352 error_ok=True,
1353 cwd=url).strip()
1354 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001356 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001357 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001358 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001359 self.issue = self._GitGetBranchConfigValue(
1360 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001361 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 return self.issue
1363
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001364 def GetIssueURL(self):
1365 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001366 issue = self.GetIssue()
1367 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001368 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001369 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001371 def GetDescription(self, pretty=False, force=False):
1372 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001374 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 self.has_description = True
1376 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001377 # Set width to 72 columns + 2 space indent.
1378 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001380 lines = self.description.splitlines()
1381 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382 return self.description
1383
1384 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001385 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001386 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001387 self.patchset = self._GitGetBranchConfigValue(
1388 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001389 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390 return self.patchset
1391
1392 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001393 """Set this branch's patchset. If patchset=0, clears the patchset."""
1394 assert self.GetBranch()
1395 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001396 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001397 else:
1398 self.patchset = int(patchset)
1399 self._GitSetBranchConfigValue(
1400 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001402 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001403 """Set this branch's issue. If issue isn't given, clears the issue."""
1404 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001406 issue = int(issue)
1407 self._GitSetBranchConfigValue(
1408 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001409 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001410 codereview_server = self._codereview_impl.GetCodereviewServer()
1411 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001412 self._GitSetBranchConfigValue(
1413 self._codereview_impl.CodereviewServerConfigKey(),
1414 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 else:
tandrii5d48c322016-08-18 16:19:37 -07001416 # Reset all of these just to be clean.
1417 reset_suffixes = [
1418 'last-upload-hash',
1419 self._codereview_impl.IssueConfigKey(),
1420 self._codereview_impl.PatchsetConfigKey(),
1421 self._codereview_impl.CodereviewServerConfigKey(),
1422 ] + self._PostUnsetIssueProperties()
1423 for prop in reset_suffixes:
1424 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001425 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001426 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427
dnjba1b0f32016-09-02 12:37:42 -07001428 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001429 if not self.GitSanityChecks(upstream_branch):
1430 DieWithError('\nGit sanity check failure')
1431
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001432 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001433 if not root:
1434 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001435 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001436
1437 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001438 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001439 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001440 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001441 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001442 except subprocess2.CalledProcessError:
1443 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001444 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001445 'This branch probably doesn\'t exist anymore. To reset the\n'
1446 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001447 ' git branch --set-upstream-to origin/master %s\n'
1448 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001449 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001450
maruel@chromium.org52424302012-08-29 15:14:30 +00001451 issue = self.GetIssue()
1452 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001453 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001454 description = self.GetDescription()
1455 else:
1456 # If the change was never uploaded, use the log messages of all commits
1457 # up to the branch point, as git cl upload will prefill the description
1458 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001459 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1460 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001461
1462 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001463 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001464 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001465 name,
1466 description,
1467 absroot,
1468 files,
1469 issue,
1470 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001471 author,
1472 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001473
dsansomee2d6fd92016-09-08 00:10:47 -07001474 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001475 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001476 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001477 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001478
1479 def RunHook(self, committing, may_prompt, verbose, change):
1480 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1481 try:
1482 return presubmit_support.DoPresubmitChecks(change, committing,
1483 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1484 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001485 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1486 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001487 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001488 DieWithError(
1489 ('%s\nMaybe your depot_tools is out of date?\n'
1490 'If all fails, contact maruel@') % e)
1491
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001492 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1493 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001494 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1495 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001496 else:
1497 # Assume url.
1498 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1499 urlparse.urlparse(issue_arg))
1500 if not parsed_issue_arg or not parsed_issue_arg.valid:
1501 DieWithError('Failed to parse issue argument "%s". '
1502 'Must be an issue number or a valid URL.' % issue_arg)
1503 return self._codereview_impl.CMDPatchWithParsedIssue(
1504 parsed_issue_arg, reject, nocommit, directory)
1505
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001506 def CMDUpload(self, options, git_diff_args, orig_args):
1507 """Uploads a change to codereview."""
1508 if git_diff_args:
1509 # TODO(ukai): is it ok for gerrit case?
1510 base_branch = git_diff_args[0]
1511 else:
1512 if self.GetBranch() is None:
1513 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1514
1515 # Default to diffing against common ancestor of upstream branch
1516 base_branch = self.GetCommonAncestorWithUpstream()
1517 git_diff_args = [base_branch, 'HEAD']
1518
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001519 # Fast best-effort checks to abort before running potentially
1520 # expensive hooks if uploading is likely to fail anyway. Passing these
1521 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001522 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001523 self._codereview_impl.EnsureCanUploadPatchset()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001524
1525 # Apply watchlists on upload.
1526 change = self.GetChange(base_branch, None)
1527 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1528 files = [f.LocalPath() for f in change.AffectedFiles()]
1529 if not options.bypass_watchlists:
1530 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1531
1532 if not options.bypass_hooks:
1533 if options.reviewers or options.tbr_owners:
1534 # Set the reviewer list now so that presubmit checks can access it.
1535 change_description = ChangeDescription(change.FullDescriptionText())
1536 change_description.update_reviewers(options.reviewers,
1537 options.tbr_owners,
1538 change)
1539 change.SetDescriptionText(change_description.description)
1540 hook_results = self.RunHook(committing=False,
1541 may_prompt=not options.force,
1542 verbose=options.verbose,
1543 change=change)
1544 if not hook_results.should_continue():
1545 return 1
1546 if not options.reviewers and hook_results.reviewers:
1547 options.reviewers = hook_results.reviewers.split(',')
1548
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001549 # TODO(tandrii): Checking local patchset against remote patchset is only
1550 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1551 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001552 latest_patchset = self.GetMostRecentPatchset()
1553 local_patchset = self.GetPatchset()
1554 if (latest_patchset and local_patchset and
1555 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001556 print('The last upload made from this repository was patchset #%d but '
1557 'the most recent patchset on the server is #%d.'
1558 % (local_patchset, latest_patchset))
1559 print('Uploading will still work, but if you\'ve uploaded to this '
1560 'issue from another machine or branch the patch you\'re '
1561 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001562 ask_for_data('About to upload; enter to confirm.')
1563
1564 print_stats(options.similarity, options.find_copies, git_diff_args)
1565 ret = self.CMDUploadChange(options, git_diff_args, change)
1566 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001567 if options.use_commit_queue:
1568 self.SetCQState(_CQState.COMMIT)
1569 elif options.cq_dry_run:
1570 self.SetCQState(_CQState.DRY_RUN)
1571
tandrii5d48c322016-08-18 16:19:37 -07001572 _git_set_branch_config_value('last-upload-hash',
1573 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001574 # Run post upload hooks, if specified.
1575 if settings.GetRunPostUploadHook():
1576 presubmit_support.DoPostUploadExecuter(
1577 change,
1578 self,
1579 settings.GetRoot(),
1580 options.verbose,
1581 sys.stdout)
1582
1583 # Upload all dependencies if specified.
1584 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001585 print()
1586 print('--dependencies has been specified.')
1587 print('All dependent local branches will be re-uploaded.')
1588 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001589 # Remove the dependencies flag from args so that we do not end up in a
1590 # loop.
1591 orig_args.remove('--dependencies')
1592 ret = upload_branch_deps(self, orig_args)
1593 return ret
1594
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001595 def SetCQState(self, new_state):
1596 """Update the CQ state for latest patchset.
1597
1598 Issue must have been already uploaded and known.
1599 """
1600 assert new_state in _CQState.ALL_STATES
1601 assert self.GetIssue()
1602 return self._codereview_impl.SetCQState(new_state)
1603
qyearsley1fdfcb62016-10-24 13:22:03 -07001604 def TriggerDryRun(self):
1605 """Triggers a dry run and prints a warning on failure."""
1606 # TODO(qyearsley): Either re-use this method in CMDset_commit
1607 # and CMDupload, or change CMDtry to trigger dry runs with
1608 # just SetCQState, and catch keyboard interrupt and other
1609 # errors in that method.
1610 try:
1611 self.SetCQState(_CQState.DRY_RUN)
1612 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1613 return 0
1614 except KeyboardInterrupt:
1615 raise
1616 except:
1617 print('WARNING: failed to trigger CQ Dry Run.\n'
1618 'Either:\n'
1619 ' * your project has no CQ\n'
1620 ' * you don\'t have permission to trigger Dry Run\n'
1621 ' * bug in this code (see stack trace below).\n'
1622 'Consider specifying which bots to trigger manually '
1623 'or asking your project owners for permissions '
1624 'or contacting Chrome Infrastructure team at '
1625 'https://www.chromium.org/infra\n\n')
1626 # Still raise exception so that stack trace is printed.
1627 raise
1628
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001629 # Forward methods to codereview specific implementation.
1630
1631 def CloseIssue(self):
1632 return self._codereview_impl.CloseIssue()
1633
1634 def GetStatus(self):
1635 return self._codereview_impl.GetStatus()
1636
1637 def GetCodereviewServer(self):
1638 return self._codereview_impl.GetCodereviewServer()
1639
tandriide281ae2016-10-12 06:02:30 -07001640 def GetIssueOwner(self):
1641 """Get owner from codereview, which may differ from this checkout."""
1642 return self._codereview_impl.GetIssueOwner()
1643
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001644 def GetApprovingReviewers(self):
1645 return self._codereview_impl.GetApprovingReviewers()
1646
1647 def GetMostRecentPatchset(self):
1648 return self._codereview_impl.GetMostRecentPatchset()
1649
tandriide281ae2016-10-12 06:02:30 -07001650 def CannotTriggerTryJobReason(self):
1651 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1652 return self._codereview_impl.CannotTriggerTryJobReason()
1653
tandrii8c5a3532016-11-04 07:52:02 -07001654 def GetTryjobProperties(self, patchset=None):
1655 """Returns dictionary of properties to launch tryjob."""
1656 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1657
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001658 def __getattr__(self, attr):
1659 # This is because lots of untested code accesses Rietveld-specific stuff
1660 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001661 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001662 # Note that child method defines __getattr__ as well, and forwards it here,
1663 # because _RietveldChangelistImpl is not cleaned up yet, and given
1664 # deprecation of Rietveld, it should probably be just removed.
1665 # Until that time, avoid infinite recursion by bypassing __getattr__
1666 # of implementation class.
1667 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001668
1669
1670class _ChangelistCodereviewBase(object):
1671 """Abstract base class encapsulating codereview specifics of a changelist."""
1672 def __init__(self, changelist):
1673 self._changelist = changelist # instance of Changelist
1674
1675 def __getattr__(self, attr):
1676 # Forward methods to changelist.
1677 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1678 # _RietveldChangelistImpl to avoid this hack?
1679 return getattr(self._changelist, attr)
1680
1681 def GetStatus(self):
1682 """Apply a rough heuristic to give a simple summary of an issue's review
1683 or CQ status, assuming adherence to a common workflow.
1684
1685 Returns None if no issue for this branch, or specific string keywords.
1686 """
1687 raise NotImplementedError()
1688
1689 def GetCodereviewServer(self):
1690 """Returns server URL without end slash, like "https://codereview.com"."""
1691 raise NotImplementedError()
1692
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001693 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001694 """Fetches and returns description from the codereview server."""
1695 raise NotImplementedError()
1696
tandrii5d48c322016-08-18 16:19:37 -07001697 @classmethod
1698 def IssueConfigKey(cls):
1699 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001700 raise NotImplementedError()
1701
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001702 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001703 def PatchsetConfigKey(cls):
1704 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001705 raise NotImplementedError()
1706
tandrii5d48c322016-08-18 16:19:37 -07001707 @classmethod
1708 def CodereviewServerConfigKey(cls):
1709 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001710 raise NotImplementedError()
1711
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001712 def _PostUnsetIssueProperties(self):
1713 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001714 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001715
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001716 def GetRieveldObjForPresubmit(self):
1717 # This is an unfortunate Rietveld-embeddedness in presubmit.
1718 # For non-Rietveld codereviews, this probably should return a dummy object.
1719 raise NotImplementedError()
1720
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001721 def GetGerritObjForPresubmit(self):
1722 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1723 return None
1724
dsansomee2d6fd92016-09-08 00:10:47 -07001725 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001726 """Update the description on codereview site."""
1727 raise NotImplementedError()
1728
1729 def CloseIssue(self):
1730 """Closes the issue."""
1731 raise NotImplementedError()
1732
1733 def GetApprovingReviewers(self):
1734 """Returns a list of reviewers approving the change.
1735
1736 Note: not necessarily committers.
1737 """
1738 raise NotImplementedError()
1739
1740 def GetMostRecentPatchset(self):
1741 """Returns the most recent patchset number from the codereview site."""
1742 raise NotImplementedError()
1743
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001744 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1745 directory):
1746 """Fetches and applies the issue.
1747
1748 Arguments:
1749 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1750 reject: if True, reject the failed patch instead of switching to 3-way
1751 merge. Rietveld only.
1752 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1753 only.
1754 directory: switch to directory before applying the patch. Rietveld only.
1755 """
1756 raise NotImplementedError()
1757
1758 @staticmethod
1759 def ParseIssueURL(parsed_url):
1760 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1761 failed."""
1762 raise NotImplementedError()
1763
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001764 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001765 """Best effort check that user is authenticated with codereview server.
1766
1767 Arguments:
1768 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001769 refresh: whether to attempt to refresh credentials. Ignored if not
1770 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001771 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001772 raise NotImplementedError()
1773
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001774 def EnsureCanUploadPatchset(self):
1775 """Best effort check that uploading isn't supposed to fail for predictable
1776 reasons.
1777
1778 This method should raise informative exception if uploading shouldn't
1779 proceed.
1780 """
1781 pass
1782
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001783 def CMDUploadChange(self, options, args, change):
1784 """Uploads a change to codereview."""
1785 raise NotImplementedError()
1786
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001787 def SetCQState(self, new_state):
1788 """Update the CQ state for latest patchset.
1789
1790 Issue must have been already uploaded and known.
1791 """
1792 raise NotImplementedError()
1793
tandriie113dfd2016-10-11 10:20:12 -07001794 def CannotTriggerTryJobReason(self):
1795 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1796 raise NotImplementedError()
1797
tandriide281ae2016-10-12 06:02:30 -07001798 def GetIssueOwner(self):
1799 raise NotImplementedError()
1800
tandrii8c5a3532016-11-04 07:52:02 -07001801 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001802 raise NotImplementedError()
1803
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001804
1805class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001806 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001807 super(_RietveldChangelistImpl, self).__init__(changelist)
1808 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001809 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001810 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001811
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001812 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001813 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001814 self._props = None
1815 self._rpc_server = None
1816
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001817 def GetCodereviewServer(self):
1818 if not self._rietveld_server:
1819 # If we're on a branch then get the server potentially associated
1820 # with that branch.
1821 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001822 self._rietveld_server = gclient_utils.UpgradeToHttps(
1823 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001824 if not self._rietveld_server:
1825 self._rietveld_server = settings.GetDefaultServerUrl()
1826 return self._rietveld_server
1827
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001828 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001829 """Best effort check that user is authenticated with Rietveld server."""
1830 if self._auth_config.use_oauth2:
1831 authenticator = auth.get_authenticator_for_host(
1832 self.GetCodereviewServer(), self._auth_config)
1833 if not authenticator.has_cached_credentials():
1834 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001835 if refresh:
1836 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001837
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001838 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001839 issue = self.GetIssue()
1840 assert issue
1841 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001842 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001843 except urllib2.HTTPError as e:
1844 if e.code == 404:
1845 DieWithError(
1846 ('\nWhile fetching the description for issue %d, received a '
1847 '404 (not found)\n'
1848 'error. It is likely that you deleted this '
1849 'issue on the server. If this is the\n'
1850 'case, please run\n\n'
1851 ' git cl issue 0\n\n'
1852 'to clear the association with the deleted issue. Then run '
1853 'this command again.') % issue)
1854 else:
1855 DieWithError(
1856 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1857 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001858 print('Warning: Failed to retrieve CL description due to network '
1859 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001860 return ''
1861
1862 def GetMostRecentPatchset(self):
1863 return self.GetIssueProperties()['patchsets'][-1]
1864
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001865 def GetIssueProperties(self):
1866 if self._props is None:
1867 issue = self.GetIssue()
1868 if not issue:
1869 self._props = {}
1870 else:
1871 self._props = self.RpcServer().get_issue_properties(issue, True)
1872 return self._props
1873
tandriie113dfd2016-10-11 10:20:12 -07001874 def CannotTriggerTryJobReason(self):
1875 props = self.GetIssueProperties()
1876 if not props:
1877 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1878 if props.get('closed'):
1879 return 'CL %s is closed' % self.GetIssue()
1880 if props.get('private'):
1881 return 'CL %s is private' % self.GetIssue()
1882 return None
1883
tandrii8c5a3532016-11-04 07:52:02 -07001884 def GetTryjobProperties(self, patchset=None):
1885 """Returns dictionary of properties to launch tryjob."""
1886 project = (self.GetIssueProperties() or {}).get('project')
1887 return {
1888 'issue': self.GetIssue(),
1889 'patch_project': project,
1890 'patch_storage': 'rietveld',
1891 'patchset': patchset or self.GetPatchset(),
1892 'rietveld': self.GetCodereviewServer(),
1893 }
1894
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001895 def GetApprovingReviewers(self):
1896 return get_approving_reviewers(self.GetIssueProperties())
1897
tandriide281ae2016-10-12 06:02:30 -07001898 def GetIssueOwner(self):
1899 return (self.GetIssueProperties() or {}).get('owner_email')
1900
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001901 def AddComment(self, message):
1902 return self.RpcServer().add_comment(self.GetIssue(), message)
1903
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001904 def GetStatus(self):
1905 """Apply a rough heuristic to give a simple summary of an issue's review
1906 or CQ status, assuming adherence to a common workflow.
1907
1908 Returns None if no issue for this branch, or one of the following keywords:
1909 * 'error' - error from review tool (including deleted issues)
1910 * 'unsent' - not sent for review
1911 * 'waiting' - waiting for review
1912 * 'reply' - waiting for owner to reply to review
1913 * 'lgtm' - LGTM from at least one approved reviewer
1914 * 'commit' - in the commit queue
1915 * 'closed' - closed
1916 """
1917 if not self.GetIssue():
1918 return None
1919
1920 try:
1921 props = self.GetIssueProperties()
1922 except urllib2.HTTPError:
1923 return 'error'
1924
1925 if props.get('closed'):
1926 # Issue is closed.
1927 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001928 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001929 # Issue is in the commit queue.
1930 return 'commit'
1931
1932 try:
1933 reviewers = self.GetApprovingReviewers()
1934 except urllib2.HTTPError:
1935 return 'error'
1936
1937 if reviewers:
1938 # Was LGTM'ed.
1939 return 'lgtm'
1940
1941 messages = props.get('messages') or []
1942
tandrii9d2c7a32016-06-22 03:42:45 -07001943 # Skip CQ messages that don't require owner's action.
1944 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1945 if 'Dry run:' in messages[-1]['text']:
1946 messages.pop()
1947 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1948 # This message always follows prior messages from CQ,
1949 # so skip this too.
1950 messages.pop()
1951 else:
1952 # This is probably a CQ messages warranting user attention.
1953 break
1954
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001955 if not messages:
1956 # No message was sent.
1957 return 'unsent'
1958 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001959 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001960 return 'reply'
1961 return 'waiting'
1962
dsansomee2d6fd92016-09-08 00:10:47 -07001963 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001964 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001965
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001966 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001967 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001968
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001969 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001970 return self.SetFlags({flag: value})
1971
1972 def SetFlags(self, flags):
1973 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001974 """
phajdan.jr68598232016-08-10 03:28:28 -07001975 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001976 try:
tandrii4b233bd2016-07-06 03:50:29 -07001977 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001978 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001979 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001980 if e.code == 404:
1981 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1982 if e.code == 403:
1983 DieWithError(
1984 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001985 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001986 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001988 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001989 """Returns an upload.RpcServer() to access this review's rietveld instance.
1990 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001991 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001992 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001993 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001994 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001995 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001996
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001997 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001998 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001999 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000
tandrii5d48c322016-08-18 16:19:37 -07002001 @classmethod
2002 def PatchsetConfigKey(cls):
2003 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002004
tandrii5d48c322016-08-18 16:19:37 -07002005 @classmethod
2006 def CodereviewServerConfigKey(cls):
2007 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002008
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002009 def GetRieveldObjForPresubmit(self):
2010 return self.RpcServer()
2011
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002012 def SetCQState(self, new_state):
2013 props = self.GetIssueProperties()
2014 if props.get('private'):
2015 DieWithError('Cannot set-commit on private issue')
2016
2017 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002018 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002019 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002020 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002021 else:
tandrii4b233bd2016-07-06 03:50:29 -07002022 assert new_state == _CQState.DRY_RUN
2023 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002024
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002025 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2026 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002027 # PatchIssue should never be called with a dirty tree. It is up to the
2028 # caller to check this, but just in case we assert here since the
2029 # consequences of the caller not checking this could be dire.
2030 assert(not git_common.is_dirty_git_tree('apply'))
2031 assert(parsed_issue_arg.valid)
2032 self._changelist.issue = parsed_issue_arg.issue
2033 if parsed_issue_arg.hostname:
2034 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2035
skobes6468b902016-10-24 08:45:10 -07002036 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2037 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2038 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002039 try:
skobes6468b902016-10-24 08:45:10 -07002040 scm_obj.apply_patch(patchset_object)
2041 except Exception as e:
2042 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002043 return 1
2044
2045 # If we had an issue, commit the current state and register the issue.
2046 if not nocommit:
2047 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2048 'patch from issue %(i)s at patchset '
2049 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2050 % {'i': self.GetIssue(), 'p': patchset})])
2051 self.SetIssue(self.GetIssue())
2052 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002053 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002054 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002055 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002056 return 0
2057
2058 @staticmethod
2059 def ParseIssueURL(parsed_url):
2060 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2061 return None
wychen3c1c1722016-08-04 11:46:36 -07002062 # Rietveld patch: https://domain/<number>/#ps<patchset>
2063 match = re.match(r'/(\d+)/$', parsed_url.path)
2064 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2065 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002066 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002067 issue=int(match.group(1)),
2068 patchset=int(match2.group(1)),
2069 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002070 # Typical url: https://domain/<issue_number>[/[other]]
2071 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2072 if match:
skobes6468b902016-10-24 08:45:10 -07002073 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002074 issue=int(match.group(1)),
2075 hostname=parsed_url.netloc)
2076 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2077 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2078 if match:
skobes6468b902016-10-24 08:45:10 -07002079 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002080 issue=int(match.group(1)),
2081 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002082 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002083 return None
2084
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002085 def CMDUploadChange(self, options, args, change):
2086 """Upload the patch to Rietveld."""
2087 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2088 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002089 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2090 if options.emulate_svn_auto_props:
2091 upload_args.append('--emulate_svn_auto_props')
2092
2093 change_desc = None
2094
2095 if options.email is not None:
2096 upload_args.extend(['--email', options.email])
2097
2098 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002099 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002100 upload_args.extend(['--title', options.title])
2101 if options.message:
2102 upload_args.extend(['--message', options.message])
2103 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002104 print('This branch is associated with issue %s. '
2105 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002106 else:
nodirca166002016-06-27 10:59:51 -07002107 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002108 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002109 if options.message:
2110 message = options.message
2111 else:
2112 message = CreateDescriptionFromLog(args)
2113 if options.title:
2114 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002115 change_desc = ChangeDescription(message)
2116 if options.reviewers or options.tbr_owners:
2117 change_desc.update_reviewers(options.reviewers,
2118 options.tbr_owners,
2119 change)
2120 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002121 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002122
2123 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002124 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002125 return 1
2126
2127 upload_args.extend(['--message', change_desc.description])
2128 if change_desc.get_reviewers():
2129 upload_args.append('--reviewers=%s' % ','.join(
2130 change_desc.get_reviewers()))
2131 if options.send_mail:
2132 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002133 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002134 upload_args.append('--send_mail')
2135
2136 # We check this before applying rietveld.private assuming that in
2137 # rietveld.cc only addresses which we can send private CLs to are listed
2138 # if rietveld.private is set, and so we should ignore rietveld.cc only
2139 # when --private is specified explicitly on the command line.
2140 if options.private:
2141 logging.warn('rietveld.cc is ignored since private flag is specified. '
2142 'You need to review and add them manually if necessary.')
2143 cc = self.GetCCListWithoutDefault()
2144 else:
2145 cc = self.GetCCList()
2146 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002147 if change_desc.get_cced():
2148 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002149 if cc:
2150 upload_args.extend(['--cc', cc])
2151
2152 if options.private or settings.GetDefaultPrivateFlag() == "True":
2153 upload_args.append('--private')
2154
2155 upload_args.extend(['--git_similarity', str(options.similarity)])
2156 if not options.find_copies:
2157 upload_args.extend(['--git_no_find_copies'])
2158
2159 # Include the upstream repo's URL in the change -- this is useful for
2160 # projects that have their source spread across multiple repos.
2161 remote_url = self.GetGitBaseUrlFromConfig()
2162 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002163 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2164 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2165 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002166 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002167 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002168 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002169 if target_ref:
2170 upload_args.extend(['--target_ref', target_ref])
2171
2172 # Look for dependent patchsets. See crbug.com/480453 for more details.
2173 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2174 upstream_branch = ShortBranchName(upstream_branch)
2175 if remote is '.':
2176 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002177 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002178 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002179 print()
2180 print('Skipping dependency patchset upload because git config '
2181 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2182 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002183 else:
2184 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002185 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002186 auth_config=auth_config)
2187 branch_cl_issue_url = branch_cl.GetIssueURL()
2188 branch_cl_issue = branch_cl.GetIssue()
2189 branch_cl_patchset = branch_cl.GetPatchset()
2190 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2191 upload_args.extend(
2192 ['--depends_on_patchset', '%s:%s' % (
2193 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002194 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002195 '\n'
2196 'The current branch (%s) is tracking a local branch (%s) with '
2197 'an associated CL.\n'
2198 'Adding %s/#ps%s as a dependency patchset.\n'
2199 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2200 branch_cl_patchset))
2201
2202 project = settings.GetProject()
2203 if project:
2204 upload_args.extend(['--project', project])
2205
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002206 try:
2207 upload_args = ['upload'] + upload_args + args
2208 logging.info('upload.RealMain(%s)', upload_args)
2209 issue, patchset = upload.RealMain(upload_args)
2210 issue = int(issue)
2211 patchset = int(patchset)
2212 except KeyboardInterrupt:
2213 sys.exit(1)
2214 except:
2215 # If we got an exception after the user typed a description for their
2216 # change, back up the description before re-raising.
2217 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002218 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002219 raise
2220
2221 if not self.GetIssue():
2222 self.SetIssue(issue)
2223 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002224 return 0
2225
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002226
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002227class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002228 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002229 # auth_config is Rietveld thing, kept here to preserve interface only.
2230 super(_GerritChangelistImpl, self).__init__(changelist)
2231 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002232 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002233 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002234 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002235 # Map from change number (issue) to its detail cache.
2236 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002237
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002238 if codereview_host is not None:
2239 assert not codereview_host.startswith('https://'), codereview_host
2240 self._gerrit_host = codereview_host
2241 self._gerrit_server = 'https://%s' % codereview_host
2242
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002243 def _GetGerritHost(self):
2244 # Lazy load of configs.
2245 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002246 if self._gerrit_host and '.' not in self._gerrit_host:
2247 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2248 # This happens for internal stuff http://crbug.com/614312.
2249 parsed = urlparse.urlparse(self.GetRemoteUrl())
2250 if parsed.scheme == 'sso':
2251 print('WARNING: using non https URLs for remote is likely broken\n'
2252 ' Your current remote is: %s' % self.GetRemoteUrl())
2253 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2254 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002255 return self._gerrit_host
2256
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002257 def _GetGitHost(self):
2258 """Returns git host to be used when uploading change to Gerrit."""
2259 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2260
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002261 def GetCodereviewServer(self):
2262 if not self._gerrit_server:
2263 # If we're on a branch then get the server potentially associated
2264 # with that branch.
2265 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002266 self._gerrit_server = self._GitGetBranchConfigValue(
2267 self.CodereviewServerConfigKey())
2268 if self._gerrit_server:
2269 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002270 if not self._gerrit_server:
2271 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2272 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002273 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002274 parts[0] = parts[0] + '-review'
2275 self._gerrit_host = '.'.join(parts)
2276 self._gerrit_server = 'https://%s' % self._gerrit_host
2277 return self._gerrit_server
2278
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002279 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002280 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002281 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002282
tandrii5d48c322016-08-18 16:19:37 -07002283 @classmethod
2284 def PatchsetConfigKey(cls):
2285 return 'gerritpatchset'
2286
2287 @classmethod
2288 def CodereviewServerConfigKey(cls):
2289 return 'gerritserver'
2290
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002291 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002292 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002293 if settings.GetGerritSkipEnsureAuthenticated():
2294 # For projects with unusual authentication schemes.
2295 # See http://crbug.com/603378.
2296 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002297 # Lazy-loader to identify Gerrit and Git hosts.
2298 if gerrit_util.GceAuthenticator.is_gce():
2299 return
2300 self.GetCodereviewServer()
2301 git_host = self._GetGitHost()
2302 assert self._gerrit_server and self._gerrit_host
2303 cookie_auth = gerrit_util.CookiesAuthenticator()
2304
2305 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2306 git_auth = cookie_auth.get_auth_header(git_host)
2307 if gerrit_auth and git_auth:
2308 if gerrit_auth == git_auth:
2309 return
2310 print((
2311 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2312 ' Check your %s or %s file for credentials of hosts:\n'
2313 ' %s\n'
2314 ' %s\n'
2315 ' %s') %
2316 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2317 git_host, self._gerrit_host,
2318 cookie_auth.get_new_password_message(git_host)))
2319 if not force:
2320 ask_for_data('If you know what you are doing, press Enter to continue, '
2321 'Ctrl+C to abort.')
2322 return
2323 else:
2324 missing = (
2325 [] if gerrit_auth else [self._gerrit_host] +
2326 [] if git_auth else [git_host])
2327 DieWithError('Credentials for the following hosts are required:\n'
2328 ' %s\n'
2329 'These are read from %s (or legacy %s)\n'
2330 '%s' % (
2331 '\n '.join(missing),
2332 cookie_auth.get_gitcookies_path(),
2333 cookie_auth.get_netrc_path(),
2334 cookie_auth.get_new_password_message(git_host)))
2335
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002336 def EnsureCanUploadPatchset(self):
2337 """Best effort check that uploading isn't supposed to fail for predictable
2338 reasons.
2339
2340 This method should raise informative exception if uploading shouldn't
2341 proceed.
2342 """
2343 if not self.GetIssue():
2344 return
2345
2346 # Warm change details cache now to avoid RPCs later, reducing latency for
2347 # developers.
2348 self.FetchDescription()
2349
2350 status = self._GetChangeDetail()['status']
2351 if status in ('MERGED', 'ABANDONED'):
2352 DieWithError('Change %s has been %s, new uploads are not allowed' %
2353 (self.GetIssueURL(),
2354 'submitted' if status == 'MERGED' else 'abandoned'))
2355
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002356 def _PostUnsetIssueProperties(self):
2357 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002358 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002359
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002360 def GetRieveldObjForPresubmit(self):
2361 class ThisIsNotRietveldIssue(object):
2362 def __nonzero__(self):
2363 # This is a hack to make presubmit_support think that rietveld is not
2364 # defined, yet still ensure that calls directly result in a decent
2365 # exception message below.
2366 return False
2367
2368 def __getattr__(self, attr):
2369 print(
2370 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2371 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2372 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2373 'or use Rietveld for codereview.\n'
2374 'See also http://crbug.com/579160.' % attr)
2375 raise NotImplementedError()
2376 return ThisIsNotRietveldIssue()
2377
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002378 def GetGerritObjForPresubmit(self):
2379 return presubmit_support.GerritAccessor(self._GetGerritHost())
2380
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002381 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002382 """Apply a rough heuristic to give a simple summary of an issue's review
2383 or CQ status, assuming adherence to a common workflow.
2384
2385 Returns None if no issue for this branch, or one of the following keywords:
2386 * 'error' - error from review tool (including deleted issues)
2387 * 'unsent' - no reviewers added
2388 * 'waiting' - waiting for review
2389 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002390 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002391 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002392 * 'commit' - in the commit queue
2393 * 'closed' - abandoned
2394 """
2395 if not self.GetIssue():
2396 return None
2397
2398 try:
2399 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002400 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002401 return 'error'
2402
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002403 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002404 return 'closed'
2405
2406 cq_label = data['labels'].get('Commit-Queue', {})
2407 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002408 votes = cq_label.get('all', [])
2409 highest_vote = 0
2410 for v in votes:
2411 highest_vote = max(highest_vote, v.get('value', 0))
2412 vote_value = str(highest_vote)
2413 if vote_value != '0':
2414 # Add a '+' if the value is not 0 to match the values in the label.
2415 # The cq_label does not have negatives.
2416 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002417 vote_text = cq_label.get('values', {}).get(vote_value, '')
2418 if vote_text.lower() == 'commit':
2419 return 'commit'
2420
2421 lgtm_label = data['labels'].get('Code-Review', {})
2422 if lgtm_label:
2423 if 'rejected' in lgtm_label:
2424 return 'not lgtm'
2425 if 'approved' in lgtm_label:
2426 return 'lgtm'
2427
2428 if not data.get('reviewers', {}).get('REVIEWER', []):
2429 return 'unsent'
2430
2431 messages = data.get('messages', [])
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002432 owner = data['owner'].get('_account_id')
2433 while messages:
2434 last_message_author = messages.pop().get('author', {})
2435 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2436 # Ignore replies from CQ.
2437 continue
Andrii Shyshkalov7afd1642017-01-29 16:07:15 +01002438 if last_message_author.get('_account_id') != owner:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002439 # Some reply from non-owner.
2440 return 'reply'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002441 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002442
2443 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002444 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002445 return data['revisions'][data['current_revision']]['_number']
2446
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002447 def FetchDescription(self, force=False):
2448 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2449 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002450 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002451 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002452
dsansomee2d6fd92016-09-08 00:10:47 -07002453 def UpdateDescriptionRemote(self, description, force=False):
2454 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2455 if not force:
2456 ask_for_data(
2457 'The description cannot be modified while the issue has a pending '
2458 'unpublished edit. Either publish the edit in the Gerrit web UI '
2459 'or delete it.\n\n'
2460 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2461
2462 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2463 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002464 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002465 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002466
2467 def CloseIssue(self):
2468 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2469
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002470 def GetApprovingReviewers(self):
2471 """Returns a list of reviewers approving the change.
2472
2473 Note: not necessarily committers.
2474 """
2475 raise NotImplementedError()
2476
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002477 def SubmitIssue(self, wait_for_merge=True):
2478 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2479 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002480
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002481 def _GetChangeDetail(self, options=None, issue=None,
2482 no_cache=False):
2483 """Returns details of the issue by querying Gerrit and caching results.
2484
2485 If fresh data is needed, set no_cache=True which will clear cache and
2486 thus new data will be fetched from Gerrit.
2487 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002488 options = options or []
2489 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002490 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002491
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002492 # Optimization to avoid multiple RPCs:
2493 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2494 'CURRENT_COMMIT' not in options):
2495 options.append('CURRENT_COMMIT')
2496
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002497 # Normalize issue and options for consistent keys in cache.
2498 issue = str(issue)
2499 options = [o.upper() for o in options]
2500
2501 # Check in cache first unless no_cache is True.
2502 if no_cache:
2503 self._detail_cache.pop(issue, None)
2504 else:
2505 options_set = frozenset(options)
2506 for cached_options_set, data in self._detail_cache.get(issue, []):
2507 # Assumption: data fetched before with extra options is suitable
2508 # for return for a smaller set of options.
2509 # For example, if we cached data for
2510 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2511 # and request is for options=[CURRENT_REVISION],
2512 # THEN we can return prior cached data.
2513 if options_set.issubset(cached_options_set):
2514 return data
2515
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002516 try:
2517 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2518 options, ignore_404=False)
2519 except gerrit_util.GerritError as e:
2520 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002521 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002522 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002523
2524 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002525 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002526
agable32978d92016-11-01 12:55:02 -07002527 def _GetChangeCommit(self, issue=None):
2528 issue = issue or self.GetIssue()
2529 assert issue, 'issue is required to query Gerrit'
2530 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2531 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002532 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002533 return data
2534
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002535 def CMDLand(self, force, bypass_hooks, verbose):
2536 if git_common.is_dirty_git_tree('land'):
2537 return 1
tandriid60367b2016-06-22 05:25:12 -07002538 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2539 if u'Commit-Queue' in detail.get('labels', {}):
2540 if not force:
2541 ask_for_data('\nIt seems this repository has a Commit Queue, '
2542 'which can test and land changes for you. '
2543 'Are you sure you wish to bypass it?\n'
2544 'Press Enter to continue, Ctrl+C to abort.')
2545
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002546 differs = True
tandriic4344b52016-08-29 06:04:54 -07002547 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002548 # Note: git diff outputs nothing if there is no diff.
2549 if not last_upload or RunGit(['diff', last_upload]).strip():
2550 print('WARNING: some changes from local branch haven\'t been uploaded')
2551 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002552 if detail['current_revision'] == last_upload:
2553 differs = False
2554 else:
2555 print('WARNING: local branch contents differ from latest uploaded '
2556 'patchset')
2557 if differs:
2558 if not force:
2559 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002560 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2561 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002562 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2563 elif not bypass_hooks:
2564 hook_results = self.RunHook(
2565 committing=True,
2566 may_prompt=not force,
2567 verbose=verbose,
2568 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2569 if not hook_results.should_continue():
2570 return 1
2571
2572 self.SubmitIssue(wait_for_merge=True)
2573 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002574 links = self._GetChangeCommit().get('web_links', [])
2575 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002576 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002577 print('Landed as %s' % link.get('url'))
2578 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002579 return 0
2580
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002581 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2582 directory):
2583 assert not reject
2584 assert not nocommit
2585 assert not directory
2586 assert parsed_issue_arg.valid
2587
2588 self._changelist.issue = parsed_issue_arg.issue
2589
2590 if parsed_issue_arg.hostname:
2591 self._gerrit_host = parsed_issue_arg.hostname
2592 self._gerrit_server = 'https://%s' % self._gerrit_host
2593
tandriic2405f52016-10-10 08:13:15 -07002594 try:
2595 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002596 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002597 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002598
2599 if not parsed_issue_arg.patchset:
2600 # Use current revision by default.
2601 revision_info = detail['revisions'][detail['current_revision']]
2602 patchset = int(revision_info['_number'])
2603 else:
2604 patchset = parsed_issue_arg.patchset
2605 for revision_info in detail['revisions'].itervalues():
2606 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2607 break
2608 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002609 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002610 (parsed_issue_arg.patchset, self.GetIssue()))
2611
2612 fetch_info = revision_info['fetch']['http']
2613 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2614 RunGit(['cherry-pick', 'FETCH_HEAD'])
2615 self.SetIssue(self.GetIssue())
2616 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002617 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002618 (self.GetIssue(), self.GetPatchset()))
2619 return 0
2620
2621 @staticmethod
2622 def ParseIssueURL(parsed_url):
2623 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2624 return None
2625 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2626 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2627 # Short urls like https://domain/<issue_number> can be used, but don't allow
2628 # specifying the patchset (you'd 404), but we allow that here.
2629 if parsed_url.path == '/':
2630 part = parsed_url.fragment
2631 else:
2632 part = parsed_url.path
2633 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2634 if match:
2635 return _ParsedIssueNumberArgument(
2636 issue=int(match.group(2)),
2637 patchset=int(match.group(4)) if match.group(4) else None,
2638 hostname=parsed_url.netloc)
2639 return None
2640
tandrii16e0b4e2016-06-07 10:34:28 -07002641 def _GerritCommitMsgHookCheck(self, offer_removal):
2642 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2643 if not os.path.exists(hook):
2644 return
2645 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2646 # custom developer made one.
2647 data = gclient_utils.FileRead(hook)
2648 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2649 return
2650 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002651 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002652 'and may interfere with it in subtle ways.\n'
2653 'We recommend you remove the commit-msg hook.')
2654 if offer_removal:
2655 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2656 if reply.lower().startswith('y'):
2657 gclient_utils.rm_file_or_tree(hook)
2658 print('Gerrit commit-msg hook removed.')
2659 else:
2660 print('OK, will keep Gerrit commit-msg hook in place.')
2661
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002662 def CMDUploadChange(self, options, args, change):
2663 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002664 if options.squash and options.no_squash:
2665 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002666
2667 if not options.squash and not options.no_squash:
2668 # Load default for user, repo, squash=true, in this order.
2669 options.squash = settings.GetSquashGerritUploads()
2670 elif options.no_squash:
2671 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002672
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002673 # We assume the remote called "origin" is the one we want.
2674 # It is probably not worthwhile to support different workflows.
2675 gerrit_remote = 'origin'
2676
2677 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002678 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002679
Aaron Gableb56ad332017-01-06 15:24:31 -08002680 # This may be None; default fallback value is determined in logic below.
2681 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002682 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002683
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002684 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002685 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002686 if self.GetIssue():
2687 # Try to get the message from a previous upload.
2688 message = self.GetDescription()
2689 if not message:
2690 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002691 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002692 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002693 if not title:
2694 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2695 title = ask_for_data(
2696 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002697 if title == default_title:
2698 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002699 change_id = self._GetChangeDetail()['change_id']
2700 while True:
2701 footer_change_ids = git_footers.get_footer_change_id(message)
2702 if footer_change_ids == [change_id]:
2703 break
2704 if not footer_change_ids:
2705 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002706 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002707 continue
2708 # There is already a valid footer but with different or several ids.
2709 # Doing this automatically is non-trivial as we don't want to lose
2710 # existing other footers, yet we want to append just 1 desired
2711 # Change-Id. Thus, just create a new footer, but let user verify the
2712 # new description.
2713 message = '%s\n\nChange-Id: %s' % (message, change_id)
2714 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002715 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002716 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002717 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002718 'Please, check the proposed correction to the description, '
2719 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2720 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2721 change_id))
2722 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2723 if not options.force:
2724 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002725 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002726 message = change_desc.description
2727 if not message:
2728 DieWithError("Description is empty. Aborting...")
2729 # Continue the while loop.
2730 # Sanity check of this code - we should end up with proper message
2731 # footer.
2732 assert [change_id] == git_footers.get_footer_change_id(message)
2733 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002734 else: # if not self.GetIssue()
2735 if options.message:
2736 message = options.message
2737 else:
2738 message = CreateDescriptionFromLog(args)
2739 if options.title:
2740 message = options.title + '\n\n' + message
2741 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002742 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002743 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002744 # On first upload, patchset title is always this string, while
2745 # --title flag gets converted to first line of message.
2746 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002747 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002748 if not change_desc.description:
2749 DieWithError("Description is empty. Aborting...")
2750 message = change_desc.description
2751 change_ids = git_footers.get_footer_change_id(message)
2752 if len(change_ids) > 1:
2753 DieWithError('too many Change-Id footers, at most 1 allowed.')
2754 if not change_ids:
2755 # Generate the Change-Id automatically.
2756 message = git_footers.add_footer_change_id(
2757 message, GenerateGerritChangeId(message))
2758 change_desc.set_description(message)
2759 change_ids = git_footers.get_footer_change_id(message)
2760 assert len(change_ids) == 1
2761 change_id = change_ids[0]
2762
2763 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2764 if remote is '.':
2765 # If our upstream branch is local, we base our squashed commit on its
2766 # squashed version.
2767 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2768 # Check the squashed hash of the parent.
2769 parent = RunGit(['config',
2770 'branch.%s.gerritsquashhash' % upstream_branch_name],
2771 error_ok=True).strip()
2772 # Verify that the upstream branch has been uploaded too, otherwise
2773 # Gerrit will create additional CLs when uploading.
2774 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2775 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002776 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002777 '\nUpload upstream branch %s first.\n'
2778 'It is likely that this branch has been rebased since its last '
2779 'upload, so you just need to upload it again.\n'
2780 '(If you uploaded it with --no-squash, then branch dependencies '
2781 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002782 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002783 else:
2784 parent = self.GetCommonAncestorWithUpstream()
2785
2786 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2787 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2788 '-m', message]).strip()
2789 else:
2790 change_desc = ChangeDescription(
2791 options.message or CreateDescriptionFromLog(args))
2792 if not change_desc.description:
2793 DieWithError("Description is empty. Aborting...")
2794
2795 if not git_footers.get_footer_change_id(change_desc.description):
2796 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002797 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2798 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002799 ref_to_push = 'HEAD'
2800 parent = '%s/%s' % (gerrit_remote, branch)
2801 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2802
2803 assert change_desc
2804 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2805 ref_to_push)]).splitlines()
2806 if len(commits) > 1:
2807 print('WARNING: This will upload %d commits. Run the following command '
2808 'to see which commits will be uploaded: ' % len(commits))
2809 print('git log %s..%s' % (parent, ref_to_push))
2810 print('You can also use `git squash-branch` to squash these into a '
2811 'single commit.')
2812 ask_for_data('About to upload; enter to confirm.')
2813
2814 if options.reviewers or options.tbr_owners:
2815 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2816 change)
2817
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002818 # Extra options that can be specified at push time. Doc:
2819 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2820 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002821 if change_desc.get_reviewers(tbr_only=True):
2822 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2823 refspec_opts.append('l=Code-Review+1')
2824
Aaron Gable9b713dd2016-12-14 16:04:21 -08002825 if title:
2826 if not re.match(r'^[\w ]+$', title):
2827 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002828 if not automatic_title:
2829 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gableeb3af712017-02-02 12:42:02 -08002830 'and spaces. You can edit it in the UI. '
2831 'See https://crbug.com/663787.\n'
2832 'Cleaned up title: %s' % title)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002833 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2834 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002835 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002836
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002837 if options.send_mail:
2838 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002839 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002840 refspec_opts.append('notify=ALL')
2841 else:
2842 refspec_opts.append('notify=NONE')
2843
tandrii99a72f22016-08-17 14:33:24 -07002844 reviewers = change_desc.get_reviewers()
2845 if reviewers:
Andrii Shyshkalovaf3a9992017-02-09 14:18:01 +01002846 # TODO(tandrii): remove this horrible hack once (Poly)Gerrit fixes their
2847 # side for real (b/34702620).
2848 def clean_invisible_chars(email):
2849 return email.decode('unicode_escape').encode('ascii', 'ignore')
2850 refspec_opts.extend('r=' + clean_invisible_chars(email).strip()
2851 for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002852
agablec6787972016-09-09 16:13:34 -07002853 if options.private:
2854 refspec_opts.append('draft')
2855
rmistry9eadede2016-09-19 11:22:43 -07002856 if options.topic:
2857 # Documentation on Gerrit topics is here:
2858 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2859 refspec_opts.append('topic=%s' % options.topic)
2860
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002861 refspec_suffix = ''
2862 if refspec_opts:
2863 refspec_suffix = '%' + ','.join(refspec_opts)
2864 assert ' ' not in refspec_suffix, (
2865 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002866 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002867
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002868 try:
2869 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalove9c78ff2017-02-06 15:53:13 +01002870 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002871 print_stdout=True,
2872 # Flush after every line: useful for seeing progress when running as
2873 # recipe.
2874 filter_fn=lambda _: sys.stdout.flush())
2875 except subprocess2.CalledProcessError:
2876 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002877 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002878
2879 if options.squash:
2880 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2881 change_numbers = [m.group(1)
2882 for m in map(regex.match, push_stdout.splitlines())
2883 if m]
2884 if len(change_numbers) != 1:
2885 DieWithError(
2886 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002887 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002888 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002889 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002890
2891 # Add cc's from the CC_LIST and --cc flag (if any).
2892 cc = self.GetCCList().split(',')
2893 if options.cc:
2894 cc.extend(options.cc)
2895 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002896 if change_desc.get_cced():
2897 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002898 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002899 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002900 self._GetGerritHost(), self.GetIssue(), cc,
2901 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002902 return 0
2903
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002904 def _AddChangeIdToCommitMessage(self, options, args):
2905 """Re-commits using the current message, assumes the commit hook is in
2906 place.
2907 """
2908 log_desc = options.message or CreateDescriptionFromLog(args)
2909 git_command = ['commit', '--amend', '-m', log_desc]
2910 RunGit(git_command)
2911 new_log_desc = CreateDescriptionFromLog(args)
2912 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002913 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002914 return new_log_desc
2915 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002916 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002917
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002918 def SetCQState(self, new_state):
2919 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002920 vote_map = {
2921 _CQState.NONE: 0,
2922 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002923 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002924 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002925 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2926 if new_state == _CQState.DRY_RUN:
2927 # Don't spam everybody reviewer/owner.
2928 kwargs['notify'] = 'NONE'
2929 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002930
tandriie113dfd2016-10-11 10:20:12 -07002931 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002932 try:
2933 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002934 except GerritChangeNotExists:
2935 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002936
2937 if data['status'] in ('ABANDONED', 'MERGED'):
2938 return 'CL %s is closed' % self.GetIssue()
2939
2940 def GetTryjobProperties(self, patchset=None):
2941 """Returns dictionary of properties to launch tryjob."""
2942 data = self._GetChangeDetail(['ALL_REVISIONS'])
2943 patchset = int(patchset or self.GetPatchset())
2944 assert patchset
2945 revision_data = None # Pylint wants it to be defined.
2946 for revision_data in data['revisions'].itervalues():
2947 if int(revision_data['_number']) == patchset:
2948 break
2949 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002950 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002951 (patchset, self.GetIssue()))
2952 return {
2953 'patch_issue': self.GetIssue(),
2954 'patch_set': patchset or self.GetPatchset(),
2955 'patch_project': data['project'],
2956 'patch_storage': 'gerrit',
2957 'patch_ref': revision_data['fetch']['http']['ref'],
2958 'patch_repository_url': revision_data['fetch']['http']['url'],
2959 'patch_gerrit_url': self.GetCodereviewServer(),
2960 }
tandriie113dfd2016-10-11 10:20:12 -07002961
tandriide281ae2016-10-12 06:02:30 -07002962 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002963 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002964
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002965
2966_CODEREVIEW_IMPLEMENTATIONS = {
2967 'rietveld': _RietveldChangelistImpl,
2968 'gerrit': _GerritChangelistImpl,
2969}
2970
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002971
iannuccie53c9352016-08-17 14:40:40 -07002972def _add_codereview_issue_select_options(parser, extra=""):
2973 _add_codereview_select_options(parser)
2974
2975 text = ('Operate on this issue number instead of the current branch\'s '
2976 'implicit issue.')
2977 if extra:
2978 text += ' '+extra
2979 parser.add_option('-i', '--issue', type=int, help=text)
2980
2981
2982def _process_codereview_issue_select_options(parser, options):
2983 _process_codereview_select_options(parser, options)
2984 if options.issue is not None and not options.forced_codereview:
2985 parser.error('--issue must be specified with either --rietveld or --gerrit')
2986
2987
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002988def _add_codereview_select_options(parser):
2989 """Appends --gerrit and --rietveld options to force specific codereview."""
2990 parser.codereview_group = optparse.OptionGroup(
2991 parser, 'EXPERIMENTAL! Codereview override options')
2992 parser.add_option_group(parser.codereview_group)
2993 parser.codereview_group.add_option(
2994 '--gerrit', action='store_true',
2995 help='Force the use of Gerrit for codereview')
2996 parser.codereview_group.add_option(
2997 '--rietveld', action='store_true',
2998 help='Force the use of Rietveld for codereview')
2999
3000
3001def _process_codereview_select_options(parser, options):
3002 if options.gerrit and options.rietveld:
3003 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3004 options.forced_codereview = None
3005 if options.gerrit:
3006 options.forced_codereview = 'gerrit'
3007 elif options.rietveld:
3008 options.forced_codereview = 'rietveld'
3009
3010
tandriif9aefb72016-07-01 09:06:51 -07003011def _get_bug_line_values(default_project, bugs):
3012 """Given default_project and comma separated list of bugs, yields bug line
3013 values.
3014
3015 Each bug can be either:
3016 * a number, which is combined with default_project
3017 * string, which is left as is.
3018
3019 This function may produce more than one line, because bugdroid expects one
3020 project per line.
3021
3022 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3023 ['v8:123', 'chromium:789']
3024 """
3025 default_bugs = []
3026 others = []
3027 for bug in bugs.split(','):
3028 bug = bug.strip()
3029 if bug:
3030 try:
3031 default_bugs.append(int(bug))
3032 except ValueError:
3033 others.append(bug)
3034
3035 if default_bugs:
3036 default_bugs = ','.join(map(str, default_bugs))
3037 if default_project:
3038 yield '%s:%s' % (default_project, default_bugs)
3039 else:
3040 yield default_bugs
3041 for other in sorted(others):
3042 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3043 yield other
3044
3045
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003046class ChangeDescription(object):
3047 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003048 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003049 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Mark Mentovai600d3092017-03-08 12:58:18 -05003050 BUG_LINE = r'^[ \t]*(BUGS?|Bugs?)[ \t]*[:=][ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003051 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003052
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003053 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003054 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003055
agable@chromium.org42c20792013-09-12 17:34:49 +00003056 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003057 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003058 return '\n'.join(self._description_lines)
3059
3060 def set_description(self, desc):
3061 if isinstance(desc, basestring):
3062 lines = desc.splitlines()
3063 else:
3064 lines = [line.rstrip() for line in desc]
3065 while lines and not lines[0]:
3066 lines.pop(0)
3067 while lines and not lines[-1]:
3068 lines.pop(-1)
3069 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003070
piman@chromium.org336f9122014-09-04 02:16:55 +00003071 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003072 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003073 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003074 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003075 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003076 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003077
agable@chromium.org42c20792013-09-12 17:34:49 +00003078 # Get the set of R= and TBR= lines and remove them from the desciption.
3079 regexp = re.compile(self.R_LINE)
3080 matches = [regexp.match(line) for line in self._description_lines]
3081 new_desc = [l for i, l in enumerate(self._description_lines)
3082 if not matches[i]]
3083 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003084
agable@chromium.org42c20792013-09-12 17:34:49 +00003085 # Construct new unified R= and TBR= lines.
3086 r_names = []
3087 tbr_names = []
3088 for match in matches:
3089 if not match:
3090 continue
3091 people = cleanup_list([match.group(2).strip()])
3092 if match.group(1) == 'TBR':
3093 tbr_names.extend(people)
3094 else:
3095 r_names.extend(people)
3096 for name in r_names:
3097 if name not in reviewers:
3098 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003099 if add_owners_tbr:
3100 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003101 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003102 all_reviewers = set(tbr_names + reviewers)
3103 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3104 all_reviewers)
3105 tbr_names.extend(owners_db.reviewers_for(missing_files,
3106 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003107 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3108 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3109
3110 # Put the new lines in the description where the old first R= line was.
3111 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3112 if 0 <= line_loc < len(self._description_lines):
3113 if new_tbr_line:
3114 self._description_lines.insert(line_loc, new_tbr_line)
3115 if new_r_line:
3116 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003117 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003118 if new_r_line:
3119 self.append_footer(new_r_line)
3120 if new_tbr_line:
3121 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003122
tandriif9aefb72016-07-01 09:06:51 -07003123 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003124 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003125 self.set_description([
3126 '# Enter a description of the change.',
3127 '# This will be displayed on the codereview site.',
3128 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003129 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003130 '--------------------',
3131 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003132
agable@chromium.org42c20792013-09-12 17:34:49 +00003133 regexp = re.compile(self.BUG_LINE)
3134 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003135 prefix = settings.GetBugPrefix()
3136 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Mark Mentovai57c47212017-03-09 11:14:09 -05003137 bug_line_format = settings.GetBugLineFormat()
tandriif9aefb72016-07-01 09:06:51 -07003138 for value in values:
Mark Mentovai57c47212017-03-09 11:14:09 -05003139 self.append_footer(bug_line_format % value)
tandriif9aefb72016-07-01 09:06:51 -07003140
agable@chromium.org42c20792013-09-12 17:34:49 +00003141 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003142 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003143 if not content:
3144 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003145 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003146
3147 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003148 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3149 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003150 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003151 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003152
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003153 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003154 """Adds a footer line to the description.
3155
3156 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3157 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3158 that Gerrit footers are always at the end.
3159 """
3160 parsed_footer_line = git_footers.parse_footer(line)
3161 if parsed_footer_line:
3162 # Line is a gerrit footer in the form: Footer-Key: any value.
3163 # Thus, must be appended observing Gerrit footer rules.
3164 self.set_description(
3165 git_footers.add_footer(self.description,
3166 key=parsed_footer_line[0],
3167 value=parsed_footer_line[1]))
3168 return
3169
3170 if not self._description_lines:
3171 self._description_lines.append(line)
3172 return
3173
3174 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3175 if gerrit_footers:
3176 # git_footers.split_footers ensures that there is an empty line before
3177 # actual (gerrit) footers, if any. We have to keep it that way.
3178 assert top_lines and top_lines[-1] == ''
3179 top_lines, separator = top_lines[:-1], top_lines[-1:]
3180 else:
3181 separator = [] # No need for separator if there are no gerrit_footers.
3182
3183 prev_line = top_lines[-1] if top_lines else ''
3184 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3185 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3186 top_lines.append('')
3187 top_lines.append(line)
3188 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003189
tandrii99a72f22016-08-17 14:33:24 -07003190 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003191 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003192 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003193 reviewers = [match.group(2).strip()
3194 for match in matches
3195 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003196 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003197
bradnelsond975b302016-10-23 12:20:23 -07003198 def get_cced(self):
3199 """Retrieves the list of reviewers."""
3200 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3201 cced = [match.group(2).strip() for match in matches if match]
3202 return cleanup_list(cced)
3203
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003204 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3205 """Updates this commit description given the parent.
3206
3207 This is essentially what Gnumbd used to do.
3208 Consult https://goo.gl/WMmpDe for more details.
3209 """
3210 assert parent_msg # No, orphan branch creation isn't supported.
3211 assert parent_hash
3212 assert dest_ref
3213 parent_footer_map = git_footers.parse_footers(parent_msg)
3214 # This will also happily parse svn-position, which GnumbD is no longer
3215 # supporting. While we'd generate correct footers, the verifier plugin
3216 # installed in Gerrit will block such commit (ie git push below will fail).
3217 parent_position = git_footers.get_position(parent_footer_map)
3218
3219 # Cherry-picks may have last line obscuring their prior footers,
3220 # from git_footers perspective. This is also what Gnumbd did.
3221 cp_line = None
3222 if (self._description_lines and
3223 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3224 cp_line = self._description_lines.pop()
3225
3226 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3227
3228 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3229 # user interference with actual footers we'd insert below.
3230 for i, (k, v) in enumerate(parsed_footers):
3231 if k.startswith('Cr-'):
3232 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3233
3234 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003235 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003236 if parent_position[0] == dest_ref:
3237 # Same branch as parent.
3238 number = int(parent_position[1]) + 1
3239 else:
3240 number = 1 # New branch, and extra lineage.
3241 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3242 int(parent_position[1])))
3243
3244 parsed_footers.append(('Cr-Commit-Position',
3245 '%s@{#%d}' % (dest_ref, number)))
3246 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3247
3248 self._description_lines = top_lines
3249 if cp_line:
3250 self._description_lines.append(cp_line)
3251 if self._description_lines[-1] != '':
3252 self._description_lines.append('') # Ensure footer separator.
3253 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3254
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003255
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003256def get_approving_reviewers(props):
3257 """Retrieves the reviewers that approved a CL from the issue properties with
3258 messages.
3259
3260 Note that the list may contain reviewers that are not committer, thus are not
3261 considered by the CQ.
3262 """
3263 return sorted(
3264 set(
3265 message['sender']
3266 for message in props['messages']
3267 if message['approval'] and message['sender'] in props['reviewers']
3268 )
3269 )
3270
3271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003272def FindCodereviewSettingsFile(filename='codereview.settings'):
3273 """Finds the given file starting in the cwd and going up.
3274
3275 Only looks up to the top of the repository unless an
3276 'inherit-review-settings-ok' file exists in the root of the repository.
3277 """
3278 inherit_ok_file = 'inherit-review-settings-ok'
3279 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003280 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003281 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3282 root = '/'
3283 while True:
3284 if filename in os.listdir(cwd):
3285 if os.path.isfile(os.path.join(cwd, filename)):
3286 return open(os.path.join(cwd, filename))
3287 if cwd == root:
3288 break
3289 cwd = os.path.dirname(cwd)
3290
3291
3292def LoadCodereviewSettingsFromFile(fileobj):
3293 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003294 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003295
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003296 def SetProperty(name, setting, unset_error_ok=False):
3297 fullname = 'rietveld.' + name
3298 if setting in keyvals:
3299 RunGit(['config', fullname, keyvals[setting]])
3300 else:
3301 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3302
tandrii48df5812016-10-17 03:55:37 -07003303 if not keyvals.get('GERRIT_HOST', False):
3304 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003305 # Only server setting is required. Other settings can be absent.
3306 # In that case, we ignore errors raised during option deletion attempt.
3307 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003308 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003309 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3310 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
Mark Mentovai57c47212017-03-09 11:14:09 -05003311 SetProperty('bug-line-format', 'BUG_LINE_FORMAT', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003312 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003313 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3314 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003315 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003316 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3317 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003318
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003319 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003320 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003321
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003322 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003323 RunGit(['config', 'gerrit.squash-uploads',
3324 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003325
tandrii@chromium.org28253532016-04-14 13:46:56 +00003326 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003327 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003328 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3329
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003330 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003331 # should be of the form
3332 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3333 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003334 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3335 keyvals['ORIGIN_URL_CONFIG']])
3336
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003337
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003338def urlretrieve(source, destination):
3339 """urllib is broken for SSL connections via a proxy therefore we
3340 can't use urllib.urlretrieve()."""
3341 with open(destination, 'w') as f:
3342 f.write(urllib2.urlopen(source).read())
3343
3344
ukai@chromium.org712d6102013-11-27 00:52:58 +00003345def hasSheBang(fname):
3346 """Checks fname is a #! script."""
3347 with open(fname) as f:
3348 return f.read(2).startswith('#!')
3349
3350
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003351# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3352def DownloadHooks(*args, **kwargs):
3353 pass
3354
3355
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003356def DownloadGerritHook(force):
3357 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003358
3359 Args:
3360 force: True to update hooks. False to install hooks if not present.
3361 """
3362 if not settings.GetIsGerrit():
3363 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003364 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003365 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3366 if not os.access(dst, os.X_OK):
3367 if os.path.exists(dst):
3368 if not force:
3369 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003370 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003371 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003372 if not hasSheBang(dst):
3373 DieWithError('Not a script: %s\n'
3374 'You need to download from\n%s\n'
3375 'into .git/hooks/commit-msg and '
3376 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003377 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3378 except Exception:
3379 if os.path.exists(dst):
3380 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003381 DieWithError('\nFailed to download hooks.\n'
3382 'You need to download from\n%s\n'
3383 'into .git/hooks/commit-msg and '
3384 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003385
3386
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003387def GetRietveldCodereviewSettingsInteractively():
3388 """Prompt the user for settings."""
3389 server = settings.GetDefaultServerUrl(error_ok=True)
3390 prompt = 'Rietveld server (host[:port])'
3391 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3392 newserver = ask_for_data(prompt + ':')
3393 if not server and not newserver:
3394 newserver = DEFAULT_SERVER
3395 if newserver:
3396 newserver = gclient_utils.UpgradeToHttps(newserver)
3397 if newserver != server:
3398 RunGit(['config', 'rietveld.server', newserver])
3399
3400 def SetProperty(initial, caption, name, is_url):
3401 prompt = caption
3402 if initial:
3403 prompt += ' ("x" to clear) [%s]' % initial
3404 new_val = ask_for_data(prompt + ':')
3405 if new_val == 'x':
3406 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3407 elif new_val:
3408 if is_url:
3409 new_val = gclient_utils.UpgradeToHttps(new_val)
3410 if new_val != initial:
3411 RunGit(['config', 'rietveld.' + name, new_val])
3412
3413 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3414 SetProperty(settings.GetDefaultPrivateFlag(),
3415 'Private flag (rietveld only)', 'private', False)
3416 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3417 'tree-status-url', False)
3418 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3419 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3420 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3421 'run-post-upload-hook', False)
3422
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003423
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003424@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003425def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003426 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003427
tandrii5d0a0422016-09-14 06:24:35 -07003428 print('WARNING: git cl config works for Rietveld only')
3429 # TODO(tandrii): remove this once we switch to Gerrit.
3430 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003431 parser.add_option('--activate-update', action='store_true',
3432 help='activate auto-updating [rietveld] section in '
3433 '.git/config')
3434 parser.add_option('--deactivate-update', action='store_true',
3435 help='deactivate auto-updating [rietveld] section in '
3436 '.git/config')
3437 options, args = parser.parse_args(args)
3438
3439 if options.deactivate_update:
3440 RunGit(['config', 'rietveld.autoupdate', 'false'])
3441 return
3442
3443 if options.activate_update:
3444 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3445 return
3446
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003447 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003448 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003449 return 0
3450
3451 url = args[0]
3452 if not url.endswith('codereview.settings'):
3453 url = os.path.join(url, 'codereview.settings')
3454
3455 # Load code review settings and download hooks (if available).
3456 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3457 return 0
3458
3459
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003460def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003461 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003462 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3463 branch = ShortBranchName(branchref)
3464 _, args = parser.parse_args(args)
3465 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003466 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003467 return RunGit(['config', 'branch.%s.base-url' % branch],
3468 error_ok=False).strip()
3469 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003470 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003471 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3472 error_ok=False).strip()
3473
3474
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003475def color_for_status(status):
3476 """Maps a Changelist status to color, for CMDstatus and other tools."""
3477 return {
3478 'unsent': Fore.RED,
3479 'waiting': Fore.BLUE,
3480 'reply': Fore.YELLOW,
3481 'lgtm': Fore.GREEN,
3482 'commit': Fore.MAGENTA,
3483 'closed': Fore.CYAN,
3484 'error': Fore.WHITE,
3485 }.get(status, Fore.WHITE)
3486
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003487
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003488def get_cl_statuses(changes, fine_grained, max_processes=None):
3489 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003490
3491 If fine_grained is true, this will fetch CL statuses from the server.
3492 Otherwise, simply indicate if there's a matching url for the given branches.
3493
3494 If max_processes is specified, it is used as the maximum number of processes
3495 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3496 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003497
3498 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003499 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003500 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003501 upload.verbosity = 0
3502
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003503 if not changes:
3504 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003505
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003506 if not fine_grained:
3507 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003508 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003509 for cl in changes:
3510 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003511 return
3512
3513 # First, sort out authentication issues.
3514 logging.debug('ensuring credentials exist')
3515 for cl in changes:
3516 cl.EnsureAuthenticated(force=False, refresh=True)
3517
3518 def fetch(cl):
3519 try:
3520 return (cl, cl.GetStatus())
3521 except:
3522 # See http://crbug.com/629863.
3523 logging.exception('failed to fetch status for %s:', cl)
3524 raise
3525
3526 threads_count = len(changes)
3527 if max_processes:
3528 threads_count = max(1, min(threads_count, max_processes))
3529 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3530
3531 pool = ThreadPool(threads_count)
3532 fetched_cls = set()
3533 try:
3534 it = pool.imap_unordered(fetch, changes).__iter__()
3535 while True:
3536 try:
3537 cl, status = it.next(timeout=5)
3538 except multiprocessing.TimeoutError:
3539 break
3540 fetched_cls.add(cl)
3541 yield cl, status
3542 finally:
3543 pool.close()
3544
3545 # Add any branches that failed to fetch.
3546 for cl in set(changes) - fetched_cls:
3547 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003548
rmistry@google.com2dd99862015-06-22 12:22:18 +00003549
3550def upload_branch_deps(cl, args):
3551 """Uploads CLs of local branches that are dependents of the current branch.
3552
3553 If the local branch dependency tree looks like:
3554 test1 -> test2.1 -> test3.1
3555 -> test3.2
3556 -> test2.2 -> test3.3
3557
3558 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3559 run on the dependent branches in this order:
3560 test2.1, test3.1, test3.2, test2.2, test3.3
3561
3562 Note: This function does not rebase your local dependent branches. Use it when
3563 you make a change to the parent branch that will not conflict with its
3564 dependent branches, and you would like their dependencies updated in
3565 Rietveld.
3566 """
3567 if git_common.is_dirty_git_tree('upload-branch-deps'):
3568 return 1
3569
3570 root_branch = cl.GetBranch()
3571 if root_branch is None:
3572 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3573 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003574 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003575 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3576 'patchset dependencies without an uploaded CL.')
3577
3578 branches = RunGit(['for-each-ref',
3579 '--format=%(refname:short) %(upstream:short)',
3580 'refs/heads'])
3581 if not branches:
3582 print('No local branches found.')
3583 return 0
3584
3585 # Create a dictionary of all local branches to the branches that are dependent
3586 # on it.
3587 tracked_to_dependents = collections.defaultdict(list)
3588 for b in branches.splitlines():
3589 tokens = b.split()
3590 if len(tokens) == 2:
3591 branch_name, tracked = tokens
3592 tracked_to_dependents[tracked].append(branch_name)
3593
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print()
3595 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003596 dependents = []
3597 def traverse_dependents_preorder(branch, padding=''):
3598 dependents_to_process = tracked_to_dependents.get(branch, [])
3599 padding += ' '
3600 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003601 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003602 dependents.append(dependent)
3603 traverse_dependents_preorder(dependent, padding)
3604 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003605 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003606
3607 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003608 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003609 return 0
3610
vapiera7fbd5a2016-06-16 09:17:49 -07003611 print('This command will checkout all dependent branches and run '
3612 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003613 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3614
andybons@chromium.org962f9462016-02-03 20:00:42 +00003615 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003616 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003617 args.extend(['-t', 'Updated patchset dependency'])
3618
rmistry@google.com2dd99862015-06-22 12:22:18 +00003619 # Record all dependents that failed to upload.
3620 failures = {}
3621 # Go through all dependents, checkout the branch and upload.
3622 try:
3623 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003624 print()
3625 print('--------------------------------------')
3626 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003627 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003628 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003629 try:
3630 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003631 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003632 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003633 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003634 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003635 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003636 finally:
3637 # Swap back to the original root branch.
3638 RunGit(['checkout', '-q', root_branch])
3639
vapiera7fbd5a2016-06-16 09:17:49 -07003640 print()
3641 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003642 for dependent_branch in dependents:
3643 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003644 print(' %s : %s' % (dependent_branch, upload_status))
3645 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003646
3647 return 0
3648
3649
kmarshall3bff56b2016-06-06 18:31:47 -07003650def CMDarchive(parser, args):
3651 """Archives and deletes branches associated with closed changelists."""
3652 parser.add_option(
3653 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003654 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003655 parser.add_option(
3656 '-f', '--force', action='store_true',
3657 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003658 parser.add_option(
3659 '-d', '--dry-run', action='store_true',
3660 help='Skip the branch tagging and removal steps.')
3661 parser.add_option(
3662 '-t', '--notags', action='store_true',
3663 help='Do not tag archived branches. '
3664 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003665
3666 auth.add_auth_options(parser)
3667 options, args = parser.parse_args(args)
3668 if args:
3669 parser.error('Unsupported args: %s' % ' '.join(args))
3670 auth_config = auth.extract_auth_config_from_options(options)
3671
3672 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3673 if not branches:
3674 return 0
3675
vapiera7fbd5a2016-06-16 09:17:49 -07003676 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003677 changes = [Changelist(branchref=b, auth_config=auth_config)
3678 for b in branches.splitlines()]
3679 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3680 statuses = get_cl_statuses(changes,
3681 fine_grained=True,
3682 max_processes=options.maxjobs)
3683 proposal = [(cl.GetBranch(),
3684 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3685 for cl, status in statuses
3686 if status == 'closed']
3687 proposal.sort()
3688
3689 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003690 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003691 return 0
3692
3693 current_branch = GetCurrentBranch()
3694
vapiera7fbd5a2016-06-16 09:17:49 -07003695 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003696 if options.notags:
3697 for next_item in proposal:
3698 print(' ' + next_item[0])
3699 else:
3700 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3701 for next_item in proposal:
3702 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003703
kmarshall9249e012016-08-23 12:02:16 -07003704 # Quit now on precondition failure or if instructed by the user, either
3705 # via an interactive prompt or by command line flags.
3706 if options.dry_run:
3707 print('\nNo changes were made (dry run).\n')
3708 return 0
3709 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003710 print('You are currently on a branch \'%s\' which is associated with a '
3711 'closed codereview issue, so archive cannot proceed. Please '
3712 'checkout another branch and run this command again.' %
3713 current_branch)
3714 return 1
kmarshall9249e012016-08-23 12:02:16 -07003715 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003716 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3717 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003718 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003719 return 1
3720
3721 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003722 if not options.notags:
3723 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003724 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003725
vapiera7fbd5a2016-06-16 09:17:49 -07003726 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003727
3728 return 0
3729
3730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003731def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003732 """Show status of changelists.
3733
3734 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003735 - Red not sent for review or broken
3736 - Blue waiting for review
3737 - Yellow waiting for you to reply to review
3738 - Green LGTM'ed
3739 - Magenta in the commit queue
3740 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003741
3742 Also see 'git cl comments'.
3743 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003744 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003745 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003746 parser.add_option('-f', '--fast', action='store_true',
3747 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003748 parser.add_option(
3749 '-j', '--maxjobs', action='store', type=int,
3750 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003751
3752 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003753 _add_codereview_issue_select_options(
3754 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003755 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003756 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003757 if args:
3758 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003759 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003760
iannuccie53c9352016-08-17 14:40:40 -07003761 if options.issue is not None and not options.field:
3762 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003763
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003764 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003765 cl = Changelist(auth_config=auth_config, issue=options.issue,
3766 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003767 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003768 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003769 elif options.field == 'id':
3770 issueid = cl.GetIssue()
3771 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003772 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003773 elif options.field == 'patch':
3774 patchset = cl.GetPatchset()
3775 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003776 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003777 elif options.field == 'status':
3778 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003779 elif options.field == 'url':
3780 url = cl.GetIssueURL()
3781 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003782 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003783 return 0
3784
3785 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3786 if not branches:
3787 print('No local branch found.')
3788 return 0
3789
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003790 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003791 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003792 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003793 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003794 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003795 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003796 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003797
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003798 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003799 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3800 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3801 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003802 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003803 c, status = output.next()
3804 branch_statuses[c.GetBranch()] = status
3805 status = branch_statuses.pop(branch)
3806 url = cl.GetIssueURL()
3807 if url and (not status or status == 'error'):
3808 # The issue probably doesn't exist anymore.
3809 url += ' (broken)'
3810
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003811 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003812 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003813 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003814 color = ''
3815 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003816 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003817 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003818 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003819 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003820
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003821
3822 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003823 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003824 print('Current branch: %s' % branch)
3825 for cl in changes:
3826 if cl.GetBranch() == branch:
3827 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003828 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003829 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003830 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003831 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003832 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003833 print('Issue description:')
3834 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003835 return 0
3836
3837
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003838def colorize_CMDstatus_doc():
3839 """To be called once in main() to add colors to git cl status help."""
3840 colors = [i for i in dir(Fore) if i[0].isupper()]
3841
3842 def colorize_line(line):
3843 for color in colors:
3844 if color in line.upper():
3845 # Extract whitespaces first and the leading '-'.
3846 indent = len(line) - len(line.lstrip(' ')) + 1
3847 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3848 return line
3849
3850 lines = CMDstatus.__doc__.splitlines()
3851 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3852
3853
phajdan.jre328cf92016-08-22 04:12:17 -07003854def write_json(path, contents):
3855 with open(path, 'w') as f:
3856 json.dump(contents, f)
3857
3858
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003859@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003860def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003861 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003862
3863 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003864 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003865 parser.add_option('-r', '--reverse', action='store_true',
3866 help='Lookup the branch(es) for the specified issues. If '
3867 'no issues are specified, all branches with mapped '
3868 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003869 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003870 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003871 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003872 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003873
dnj@chromium.org406c4402015-03-03 17:22:28 +00003874 if options.reverse:
3875 branches = RunGit(['for-each-ref', 'refs/heads',
3876 '--format=%(refname:short)']).splitlines()
3877
3878 # Reverse issue lookup.
3879 issue_branch_map = {}
3880 for branch in branches:
3881 cl = Changelist(branchref=branch)
3882 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3883 if not args:
3884 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003885 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003886 for issue in args:
3887 if not issue:
3888 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003889 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003890 print('Branch for issue number %s: %s' % (
3891 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003892 if options.json:
3893 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003894 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003895 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003896 if len(args) > 0:
3897 try:
3898 issue = int(args[0])
3899 except ValueError:
3900 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003901 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003902 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003903 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003904 if options.json:
3905 write_json(options.json, {
3906 'issue': cl.GetIssue(),
3907 'issue_url': cl.GetIssueURL(),
3908 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003909 return 0
3910
3911
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003912def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003913 """Shows or posts review comments for any changelist."""
3914 parser.add_option('-a', '--add-comment', dest='comment',
3915 help='comment to add to an issue')
3916 parser.add_option('-i', dest='issue',
3917 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003918 parser.add_option('-j', '--json-file',
3919 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003920 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003921 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003922 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003923
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003924 issue = None
3925 if options.issue:
3926 try:
3927 issue = int(options.issue)
3928 except ValueError:
3929 DieWithError('A review issue id is expected to be a number')
3930
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003931 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003932
3933 if options.comment:
3934 cl.AddComment(options.comment)
3935 return 0
3936
3937 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003938 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003939 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003940 summary.append({
3941 'date': message['date'],
3942 'lgtm': False,
3943 'message': message['text'],
3944 'not_lgtm': False,
3945 'sender': message['sender'],
3946 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003947 if message['disapproval']:
3948 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003949 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003950 elif message['approval']:
3951 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003952 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003953 elif message['sender'] == data['owner_email']:
3954 color = Fore.MAGENTA
3955 else:
3956 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003957 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003958 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003959 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003960 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003961 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003962 if options.json_file:
3963 with open(options.json_file, 'wb') as f:
3964 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003965 return 0
3966
3967
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003968@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003969def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003970 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003971 parser.add_option('-d', '--display', action='store_true',
3972 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003973 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003974 help='New description to set for this issue (- for stdin, '
3975 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003976 parser.add_option('-f', '--force', action='store_true',
3977 help='Delete any unpublished Gerrit edits for this issue '
3978 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003979
3980 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003981 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003982 options, args = parser.parse_args(args)
3983 _process_codereview_select_options(parser, options)
3984
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003985 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003986 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003987 target_issue_arg = ParseIssueNumberArgument(args[0])
3988 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003989 parser.print_help()
3990 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003991
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003992 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003993
martiniss6eda05f2016-06-30 10:18:35 -07003994 kwargs = {
3995 'auth_config': auth_config,
3996 'codereview': options.forced_codereview,
3997 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003998 if target_issue_arg:
3999 kwargs['issue'] = target_issue_arg.issue
4000 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004001
4002 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004003
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004004 if not cl.GetIssue():
4005 DieWithError('This branch has no associated changelist.')
4006 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004007
smut@google.com34fb6b12015-07-13 20:03:26 +00004008 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004009 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004010 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004011
4012 if options.new_description:
4013 text = options.new_description
4014 if text == '-':
4015 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004016 elif text == '+':
4017 base_branch = cl.GetCommonAncestorWithUpstream()
4018 change = cl.GetChange(base_branch, None, local_description=True)
4019 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004020
4021 description.set_description(text)
4022 else:
4023 description.prompt()
4024
wychen@chromium.org063e4e52015-04-03 06:51:44 +00004025 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004026 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004027 return 0
4028
4029
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004030def CreateDescriptionFromLog(args):
4031 """Pulls out the commit log to use as a base for the CL description."""
4032 log_args = []
4033 if len(args) == 1 and not args[0].endswith('.'):
4034 log_args = [args[0] + '..']
4035 elif len(args) == 1 and args[0].endswith('...'):
4036 log_args = [args[0][:-1]]
4037 elif len(args) == 2:
4038 log_args = [args[0] + '..' + args[1]]
4039 else:
4040 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004041 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004042
4043
thestig@chromium.org44202a22014-03-11 19:22:18 +00004044def CMDlint(parser, args):
4045 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004046 parser.add_option('--filter', action='append', metavar='-x,+y',
4047 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004048 auth.add_auth_options(parser)
4049 options, args = parser.parse_args(args)
4050 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004051
4052 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004053 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004054 try:
4055 import cpplint
4056 import cpplint_chromium
4057 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004058 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004059 return 1
4060
4061 # Change the current working directory before calling lint so that it
4062 # shows the correct base.
4063 previous_cwd = os.getcwd()
4064 os.chdir(settings.GetRoot())
4065 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004066 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004067 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4068 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004069 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004070 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004071 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004072
4073 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004074 command = args + files
4075 if options.filter:
4076 command = ['--filter=' + ','.join(options.filter)] + command
4077 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004078
4079 white_regex = re.compile(settings.GetLintRegex())
4080 black_regex = re.compile(settings.GetLintIgnoreRegex())
4081 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4082 for filename in filenames:
4083 if white_regex.match(filename):
4084 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004085 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004086 else:
4087 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4088 extra_check_functions)
4089 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004090 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004091 finally:
4092 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004093 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004094 if cpplint._cpplint_state.error_count != 0:
4095 return 1
4096 return 0
4097
4098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004099def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004100 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004101 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004102 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004103 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004104 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004105 auth.add_auth_options(parser)
4106 options, args = parser.parse_args(args)
4107 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004108
sbc@chromium.org71437c02015-04-09 19:29:40 +00004109 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004110 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004111 return 1
4112
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004113 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004114 if args:
4115 base_branch = args[0]
4116 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004117 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004118 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004119
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004120 cl.RunHook(
4121 committing=not options.upload,
4122 may_prompt=False,
4123 verbose=options.verbose,
4124 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004125 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004126
4127
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004128def GenerateGerritChangeId(message):
4129 """Returns Ixxxxxx...xxx change id.
4130
4131 Works the same way as
4132 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4133 but can be called on demand on all platforms.
4134
4135 The basic idea is to generate git hash of a state of the tree, original commit
4136 message, author/committer info and timestamps.
4137 """
4138 lines = []
4139 tree_hash = RunGitSilent(['write-tree'])
4140 lines.append('tree %s' % tree_hash.strip())
4141 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4142 if code == 0:
4143 lines.append('parent %s' % parent.strip())
4144 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4145 lines.append('author %s' % author.strip())
4146 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4147 lines.append('committer %s' % committer.strip())
4148 lines.append('')
4149 # Note: Gerrit's commit-hook actually cleans message of some lines and
4150 # whitespace. This code is not doing this, but it clearly won't decrease
4151 # entropy.
4152 lines.append(message)
4153 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4154 stdin='\n'.join(lines))
4155 return 'I%s' % change_hash.strip()
4156
4157
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004158def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004159 """Computes the remote branch ref to use for the CL.
4160
4161 Args:
4162 remote (str): The git remote for the CL.
4163 remote_branch (str): The git remote branch for the CL.
4164 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004165 """
4166 if not (remote and remote_branch):
4167 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004168
wittman@chromium.org455dc922015-01-26 20:15:50 +00004169 if target_branch:
4170 # Cannonicalize branch references to the equivalent local full symbolic
4171 # refs, which are then translated into the remote full symbolic refs
4172 # below.
4173 if '/' not in target_branch:
4174 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4175 else:
4176 prefix_replacements = (
4177 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4178 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4179 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4180 )
4181 match = None
4182 for regex, replacement in prefix_replacements:
4183 match = re.search(regex, target_branch)
4184 if match:
4185 remote_branch = target_branch.replace(match.group(0), replacement)
4186 break
4187 if not match:
4188 # This is a branch path but not one we recognize; use as-is.
4189 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004190 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4191 # Handle the refs that need to land in different refs.
4192 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004193
wittman@chromium.org455dc922015-01-26 20:15:50 +00004194 # Create the true path to the remote branch.
4195 # Does the following translation:
4196 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4197 # * refs/remotes/origin/master -> refs/heads/master
4198 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4199 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4200 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4201 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4202 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4203 'refs/heads/')
4204 elif remote_branch.startswith('refs/remotes/branch-heads'):
4205 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004206
wittman@chromium.org455dc922015-01-26 20:15:50 +00004207 return remote_branch
4208
4209
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004210def cleanup_list(l):
4211 """Fixes a list so that comma separated items are put as individual items.
4212
4213 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4214 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4215 """
4216 items = sum((i.split(',') for i in l), [])
4217 stripped_items = (i.strip() for i in items)
4218 return sorted(filter(None, stripped_items))
4219
4220
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004221@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004222def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004223 """Uploads the current changelist to codereview.
4224
4225 Can skip dependency patchset uploads for a branch by running:
4226 git config branch.branch_name.skip-deps-uploads True
4227 To unset run:
4228 git config --unset branch.branch_name.skip-deps-uploads
4229 Can also set the above globally by using the --global flag.
4230 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004231 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4232 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004233 parser.add_option('--bypass-watchlists', action='store_true',
4234 dest='bypass_watchlists',
4235 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004236 parser.add_option('-f', action='store_true', dest='force',
4237 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004238 parser.add_option('--message', '-m', dest='message',
4239 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004240 parser.add_option('-b', '--bug',
4241 help='pre-populate the bug number(s) for this issue. '
4242 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004243 parser.add_option('--message-file', dest='message_file',
4244 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004245 parser.add_option('--title', '-t', dest='title',
4246 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004247 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004248 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004249 help='reviewer email addresses')
4250 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004251 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004252 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004253 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004254 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004255 parser.add_option('--emulate_svn_auto_props',
4256 '--emulate-svn-auto-props',
4257 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004258 dest="emulate_svn_auto_props",
4259 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004260 parser.add_option('-c', '--use-commit-queue', action='store_true',
4261 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004262 parser.add_option('--private', action='store_true',
4263 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004264 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004265 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004266 metavar='TARGET',
4267 help='Apply CL to remote ref TARGET. ' +
4268 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004269 parser.add_option('--squash', action='store_true',
4270 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004271 parser.add_option('--no-squash', action='store_true',
4272 help='Don\'t squash multiple commits into one ' +
4273 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004274 parser.add_option('--topic', default=None,
4275 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004276 parser.add_option('--email', default=None,
4277 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004278 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4279 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004280 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4281 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004282 help='Send the patchset to do a CQ dry run right after '
4283 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004284 parser.add_option('--dependencies', action='store_true',
4285 help='Uploads CLs of all the local branches that depend on '
4286 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004287
rmistry@google.com2dd99862015-06-22 12:22:18 +00004288 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004289 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004290 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004291 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004292 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004293 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004294 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004295
sbc@chromium.org71437c02015-04-09 19:29:40 +00004296 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004297 return 1
4298
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004299 options.reviewers = cleanup_list(options.reviewers)
4300 options.cc = cleanup_list(options.cc)
4301
tandriib80458a2016-06-23 12:20:07 -07004302 if options.message_file:
4303 if options.message:
4304 parser.error('only one of --message and --message-file allowed.')
4305 options.message = gclient_utils.FileRead(options.message_file)
4306 options.message_file = None
4307
tandrii4d0545a2016-07-06 03:56:49 -07004308 if options.cq_dry_run and options.use_commit_queue:
4309 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4310
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004311 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4312 settings.GetIsGerrit()
4313
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004314 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004315 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004316
4317
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004318@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004319def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004320 """DEPRECATED: Used to commit the current changelist via git-svn."""
4321 message = ('git-cl no longer supports committing to SVN repositories via '
4322 'git-svn. You probably want to use `git cl land` instead.')
4323 print(message)
4324 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004325
4326
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004327@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004328def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004329 """Commits the current changelist via git.
4330
4331 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4332 upstream and closes the issue automatically and atomically.
4333
4334 Otherwise (in case of Rietveld):
4335 Squashes branch into a single commit.
4336 Updates commit message with metadata (e.g. pointer to review).
4337 Pushes the code upstream.
4338 Updates review and closes.
4339 """
4340 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4341 help='bypass upload presubmit hook')
4342 parser.add_option('-m', dest='message',
4343 help="override review description")
4344 parser.add_option('-f', action='store_true', dest='force',
4345 help="force yes to questions (don't prompt)")
4346 parser.add_option('-c', dest='contributor',
4347 help="external contributor for patch (appended to " +
4348 "description and used as author for git). Should be " +
4349 "formatted as 'First Last <email@example.com>'")
4350 add_git_similarity(parser)
4351 auth.add_auth_options(parser)
4352 (options, args) = parser.parse_args(args)
4353 auth_config = auth.extract_auth_config_from_options(options)
4354
4355 cl = Changelist(auth_config=auth_config)
4356
4357 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4358 if cl.IsGerrit():
4359 if options.message:
4360 # This could be implemented, but it requires sending a new patch to
4361 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4362 # Besides, Gerrit has the ability to change the commit message on submit
4363 # automatically, thus there is no need to support this option (so far?).
4364 parser.error('-m MESSAGE option is not supported for Gerrit.')
4365 if options.contributor:
4366 parser.error(
4367 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4368 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4369 'the contributor\'s "name <email>". If you can\'t upload such a '
4370 'commit for review, contact your repository admin and request'
4371 '"Forge-Author" permission.')
4372 if not cl.GetIssue():
4373 DieWithError('You must upload the change first to Gerrit.\n'
4374 ' If you would rather have `git cl land` upload '
4375 'automatically for you, see http://crbug.com/642759')
4376 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4377 options.verbose)
4378
4379 current = cl.GetBranch()
4380 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4381 if remote == '.':
4382 print()
4383 print('Attempting to push branch %r into another local branch!' % current)
4384 print()
4385 print('Either reparent this branch on top of origin/master:')
4386 print(' git reparent-branch --root')
4387 print()
4388 print('OR run `git rebase-update` if you think the parent branch is ')
4389 print('already committed.')
4390 print()
4391 print(' Current parent: %r' % upstream_branch)
4392 return 1
4393
4394 if not args:
4395 # Default to merging against our best guess of the upstream branch.
4396 args = [cl.GetUpstreamBranch()]
4397
4398 if options.contributor:
4399 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4400 print("Please provide contibutor as 'First Last <email@example.com>'")
4401 return 1
4402
4403 base_branch = args[0]
4404
4405 if git_common.is_dirty_git_tree('land'):
4406 return 1
4407
4408 # This rev-list syntax means "show all commits not in my branch that
4409 # are in base_branch".
4410 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4411 base_branch]).splitlines()
4412 if upstream_commits:
4413 print('Base branch "%s" has %d commits '
4414 'not in this branch.' % (base_branch, len(upstream_commits)))
4415 print('Run "git merge %s" before attempting to land.' % base_branch)
4416 return 1
4417
4418 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4419 if not options.bypass_hooks:
4420 author = None
4421 if options.contributor:
4422 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4423 hook_results = cl.RunHook(
4424 committing=True,
4425 may_prompt=not options.force,
4426 verbose=options.verbose,
4427 change=cl.GetChange(merge_base, author))
4428 if not hook_results.should_continue():
4429 return 1
4430
4431 # Check the tree status if the tree status URL is set.
4432 status = GetTreeStatus()
4433 if 'closed' == status:
4434 print('The tree is closed. Please wait for it to reopen. Use '
4435 '"git cl land --bypass-hooks" to commit on a closed tree.')
4436 return 1
4437 elif 'unknown' == status:
4438 print('Unable to determine tree status. Please verify manually and '
4439 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4440 return 1
4441
4442 change_desc = ChangeDescription(options.message)
4443 if not change_desc.description and cl.GetIssue():
4444 change_desc = ChangeDescription(cl.GetDescription())
4445
4446 if not change_desc.description:
4447 if not cl.GetIssue() and options.bypass_hooks:
4448 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4449 else:
4450 print('No description set.')
4451 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4452 return 1
4453
4454 # Keep a separate copy for the commit message, because the commit message
4455 # contains the link to the Rietveld issue, while the Rietveld message contains
4456 # the commit viewvc url.
4457 if cl.GetIssue():
4458 change_desc.update_reviewers(cl.GetApprovingReviewers())
4459
4460 commit_desc = ChangeDescription(change_desc.description)
4461 if cl.GetIssue():
4462 # Xcode won't linkify this URL unless there is a non-whitespace character
4463 # after it. Add a period on a new line to circumvent this. Also add a space
4464 # before the period to make sure that Gitiles continues to correctly resolve
4465 # the URL.
4466 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4467 if options.contributor:
4468 commit_desc.append_footer('Patch from %s.' % options.contributor)
4469
4470 print('Description:')
4471 print(commit_desc.description)
4472
4473 branches = [merge_base, cl.GetBranchRef()]
4474 if not options.force:
4475 print_stats(options.similarity, options.find_copies, branches)
4476
4477 # We want to squash all this branch's commits into one commit with the proper
4478 # description. We do this by doing a "reset --soft" to the base branch (which
4479 # keeps the working copy the same), then landing that.
4480 MERGE_BRANCH = 'git-cl-commit'
4481 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4482 # Delete the branches if they exist.
4483 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4484 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4485 result = RunGitWithCode(showref_cmd)
4486 if result[0] == 0:
4487 RunGit(['branch', '-D', branch])
4488
4489 # We might be in a directory that's present in this branch but not in the
4490 # trunk. Move up to the top of the tree so that git commands that expect a
4491 # valid CWD won't fail after we check out the merge branch.
4492 rel_base_path = settings.GetRelativeRoot()
4493 if rel_base_path:
4494 os.chdir(rel_base_path)
4495
4496 # Stuff our change into the merge branch.
4497 # We wrap in a try...finally block so if anything goes wrong,
4498 # we clean up the branches.
4499 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004500 revision = None
4501 try:
4502 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4503 RunGit(['reset', '--soft', merge_base])
4504 if options.contributor:
4505 RunGit(
4506 [
4507 'commit', '--author', options.contributor,
4508 '-m', commit_desc.description,
4509 ])
4510 else:
4511 RunGit(['commit', '-m', commit_desc.description])
4512
4513 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4514 mirror = settings.GetGitMirror(remote)
4515 if mirror:
4516 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004517 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004518 else:
4519 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004520 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004521 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4522
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004523 if git_numberer_enabled:
4524 # TODO(tandrii): maybe do autorebase + retry on failure
4525 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004526 logging.debug('Adding git number footers')
4527 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4528 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4529 branch)
4530 # Ensure timestamps are monotonically increasing.
4531 timestamp = max(1 + _get_committer_timestamp(merge_base),
4532 _get_committer_timestamp('HEAD'))
4533 _git_amend_head(commit_desc.description, timestamp)
4534 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004535
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004536 retcode, output = RunGitWithCode(
4537 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004538 if retcode == 0:
4539 revision = RunGit(['rev-parse', 'HEAD']).strip()
4540 logging.debug(output)
4541 except: # pylint: disable=bare-except
4542 if _IS_BEING_TESTED:
4543 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4544 + '-' * 30 + '8<' + '-' * 30)
4545 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4546 raise
4547 finally:
4548 # And then swap back to the original branch and clean up.
4549 RunGit(['checkout', '-q', cl.GetBranch()])
4550 RunGit(['branch', '-D', MERGE_BRANCH])
4551
4552 if not revision:
4553 print('Failed to push. If this persists, please file a bug.')
4554 return 1
4555
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004556 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004557 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004558 if viewvc_url and revision:
4559 change_desc.append_footer(
4560 'Committed: %s%s' % (viewvc_url, revision))
4561 elif revision:
4562 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004563 print('Closing issue '
4564 '(you may be prompted for your codereview password)...')
4565 cl.UpdateDescription(change_desc.description)
4566 cl.CloseIssue()
4567 props = cl.GetIssueProperties()
4568 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004569 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4570 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004571 if options.bypass_hooks:
4572 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4573 else:
4574 comment += ' (presubmit successful).'
4575 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4576
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004577 if os.path.isfile(POSTUPSTREAM_HOOK):
4578 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4579
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004580 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004581
4582
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004583@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004584def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004585 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004586 parser.add_option('-b', dest='newbranch',
4587 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004588 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004589 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004590 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4591 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004592 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004593 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004594 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004595 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004596 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004597 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004598
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004599
4600 group = optparse.OptionGroup(
4601 parser,
4602 'Options for continuing work on the current issue uploaded from a '
4603 'different clone (e.g. different machine). Must be used independently '
4604 'from the other options. No issue number should be specified, and the '
4605 'branch must have an issue number associated with it')
4606 group.add_option('--reapply', action='store_true', dest='reapply',
4607 help='Reset the branch and reapply the issue.\n'
4608 'CAUTION: This will undo any local changes in this '
4609 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004610
4611 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004612 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004613 parser.add_option_group(group)
4614
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004615 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004616 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004617 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004618 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004619 auth_config = auth.extract_auth_config_from_options(options)
4620
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004621
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004622 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004623 if options.newbranch:
4624 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004625 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004626 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004627
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004628 cl = Changelist(auth_config=auth_config,
4629 codereview=options.forced_codereview)
4630 if not cl.GetIssue():
4631 parser.error('current branch must have an associated issue')
4632
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004633 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004634 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004635 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004636
4637 RunGit(['reset', '--hard', upstream])
4638 if options.pull:
4639 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004640
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004641 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4642 options.directory)
4643
4644 if len(args) != 1 or not args[0]:
4645 parser.error('Must specify issue number or url')
4646
4647 # We don't want uncommitted changes mixed up with the patch.
4648 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004649 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004650
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004651 if options.newbranch:
4652 if options.force:
4653 RunGit(['branch', '-D', options.newbranch],
4654 stderr=subprocess2.PIPE, error_ok=True)
4655 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004656 elif not GetCurrentBranch():
4657 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004658
4659 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4660
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004661 if cl.IsGerrit():
4662 if options.reject:
4663 parser.error('--reject is not supported with Gerrit codereview.')
4664 if options.nocommit:
4665 parser.error('--nocommit is not supported with Gerrit codereview.')
4666 if options.directory:
4667 parser.error('--directory is not supported with Gerrit codereview.')
4668
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004669 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004670 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004671
4672
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004673def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004674 """Fetches the tree status and returns either 'open', 'closed',
4675 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004676 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004677 if url:
4678 status = urllib2.urlopen(url).read().lower()
4679 if status.find('closed') != -1 or status == '0':
4680 return 'closed'
4681 elif status.find('open') != -1 or status == '1':
4682 return 'open'
4683 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004684 return 'unset'
4685
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004686
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004687def GetTreeStatusReason():
4688 """Fetches the tree status from a json url and returns the message
4689 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004690 url = settings.GetTreeStatusUrl()
4691 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004692 connection = urllib2.urlopen(json_url)
4693 status = json.loads(connection.read())
4694 connection.close()
4695 return status['message']
4696
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004698def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004699 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004700 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004701 status = GetTreeStatus()
4702 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004703 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004704 return 2
4705
vapiera7fbd5a2016-06-16 09:17:49 -07004706 print('The tree is %s' % status)
4707 print()
4708 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004709 if status != 'open':
4710 return 1
4711 return 0
4712
4713
maruel@chromium.org15192402012-09-06 12:38:29 +00004714def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004715 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004716 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004717 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004718 '-b', '--bot', action='append',
4719 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4720 'times to specify multiple builders. ex: '
4721 '"-b win_rel -b win_layout". See '
4722 'the try server waterfall for the builders name and the tests '
4723 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004724 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004725 '-B', '--bucket', default='',
4726 help=('Buildbucket bucket to send the try requests.'))
4727 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004728 '-m', '--master', default='',
4729 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004730 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004731 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004732 help='Revision to use for the try job; default: the revision will '
4733 'be determined by the try recipe that builder runs, which usually '
4734 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004735 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004736 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004737 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004738 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004739 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004740 '--project',
4741 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004742 'in recipe to determine to which repository or directory to '
4743 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004744 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004745 '-p', '--property', dest='properties', action='append', default=[],
4746 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004747 'key2=value2 etc. The value will be treated as '
4748 'json if decodable, or as string otherwise. '
4749 'NOTE: using this may make your try job not usable for CQ, '
4750 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004751 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004752 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4753 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004754 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004755 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004756 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004757 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004758
machenbach@chromium.org45453142015-09-15 08:45:22 +00004759 # Make sure that all properties are prop=value pairs.
4760 bad_params = [x for x in options.properties if '=' not in x]
4761 if bad_params:
4762 parser.error('Got properties with missing "=": %s' % bad_params)
4763
maruel@chromium.org15192402012-09-06 12:38:29 +00004764 if args:
4765 parser.error('Unknown arguments: %s' % args)
4766
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004767 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004768 if not cl.GetIssue():
4769 parser.error('Need to upload first')
4770
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004771 if cl.IsGerrit():
4772 # HACK: warm up Gerrit change detail cache to save on RPCs.
4773 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4774
tandriie113dfd2016-10-11 10:20:12 -07004775 error_message = cl.CannotTriggerTryJobReason()
4776 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004777 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004778
borenet6c0efe62016-10-19 08:13:29 -07004779 if options.bucket and options.master:
4780 parser.error('Only one of --bucket and --master may be used.')
4781
qyearsley1fdfcb62016-10-24 13:22:03 -07004782 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004783
qyearsleydd49f942016-10-28 11:57:22 -07004784 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4785 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004786 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004787 if options.verbose:
4788 print('git cl try with no bots now defaults to CQ Dry Run.')
4789 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004790
borenet6c0efe62016-10-19 08:13:29 -07004791 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004792 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004793 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004794 'of bot requires an initial job from a parent (usually a builder). '
4795 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004796 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004797 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004798
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004799 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004800 # TODO(tandrii): Checking local patchset against remote patchset is only
4801 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4802 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004803 print('Warning: Codereview server has newer patchsets (%s) than most '
4804 'recent upload from local checkout (%s). Did a previous upload '
4805 'fail?\n'
4806 'By default, git cl try uses the latest patchset from '
4807 'codereview, continuing to use patchset %s.\n' %
4808 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004809
tandrii568043b2016-10-11 07:49:18 -07004810 try:
borenet6c0efe62016-10-19 08:13:29 -07004811 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4812 patchset)
tandrii568043b2016-10-11 07:49:18 -07004813 except BuildbucketResponseException as ex:
4814 print('ERROR: %s' % ex)
4815 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004816 return 0
4817
4818
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004819def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004820 """Prints info about try jobs associated with current CL."""
4821 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004822 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004823 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004824 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004825 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004826 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004827 '--color', action='store_true', default=setup_color.IS_TTY,
4828 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004829 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004830 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4831 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004832 group.add_option(
4833 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004834 parser.add_option_group(group)
4835 auth.add_auth_options(parser)
4836 options, args = parser.parse_args(args)
4837 if args:
4838 parser.error('Unrecognized args: %s' % ' '.join(args))
4839
4840 auth_config = auth.extract_auth_config_from_options(options)
4841 cl = Changelist(auth_config=auth_config)
4842 if not cl.GetIssue():
4843 parser.error('Need to upload first')
4844
tandrii221ab252016-10-06 08:12:04 -07004845 patchset = options.patchset
4846 if not patchset:
4847 patchset = cl.GetMostRecentPatchset()
4848 if not patchset:
4849 parser.error('Codereview doesn\'t know about issue %s. '
4850 'No access to issue or wrong issue number?\n'
4851 'Either upload first, or pass --patchset explicitely' %
4852 cl.GetIssue())
4853
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004854 # TODO(tandrii): Checking local patchset against remote patchset is only
4855 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4856 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004857 print('Warning: Codereview server has newer patchsets (%s) than most '
4858 'recent upload from local checkout (%s). Did a previous upload '
4859 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004860 'By default, git cl try-results uses the latest patchset from '
4861 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004862 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004863 try:
tandrii221ab252016-10-06 08:12:04 -07004864 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004865 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004866 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004867 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004868 if options.json:
4869 write_try_results_json(options.json, jobs)
4870 else:
4871 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004872 return 0
4873
4874
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004875@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004876def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004877 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004878 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004879 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004880 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004881
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004882 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004883 if args:
4884 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004885 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004886 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004887 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004888 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004889
4890 # Clear configured merge-base, if there is one.
4891 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004892 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004893 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004894 return 0
4895
4896
thestig@chromium.org00858c82013-12-02 23:08:03 +00004897def CMDweb(parser, args):
4898 """Opens the current CL in the web browser."""
4899 _, args = parser.parse_args(args)
4900 if args:
4901 parser.error('Unrecognized args: %s' % ' '.join(args))
4902
4903 issue_url = Changelist().GetIssueURL()
4904 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004905 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004906 return 1
4907
4908 webbrowser.open(issue_url)
4909 return 0
4910
4911
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004912def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004913 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004914 parser.add_option('-d', '--dry-run', action='store_true',
4915 help='trigger in dry run mode')
4916 parser.add_option('-c', '--clear', action='store_true',
4917 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004918 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004919 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004920 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004921 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004922 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004923 if args:
4924 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004925 if options.dry_run and options.clear:
4926 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4927
iannuccie53c9352016-08-17 14:40:40 -07004928 cl = Changelist(auth_config=auth_config, issue=options.issue,
4929 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004930 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004931 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004932 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004933 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004934 state = _CQState.DRY_RUN
4935 else:
4936 state = _CQState.COMMIT
4937 if not cl.GetIssue():
4938 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004939 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004940 return 0
4941
4942
groby@chromium.org411034a2013-02-26 15:12:01 +00004943def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004944 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004945 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004946 auth.add_auth_options(parser)
4947 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004948 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004949 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004950 if args:
4951 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004952 cl = Changelist(auth_config=auth_config, issue=options.issue,
4953 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004954 # Ensure there actually is an issue to close.
4955 cl.GetDescription()
4956 cl.CloseIssue()
4957 return 0
4958
4959
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004960def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004961 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004962 parser.add_option(
4963 '--stat',
4964 action='store_true',
4965 dest='stat',
4966 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004967 auth.add_auth_options(parser)
4968 options, args = parser.parse_args(args)
4969 auth_config = auth.extract_auth_config_from_options(options)
4970 if args:
4971 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004972
4973 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004974 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004975 # Staged changes would be committed along with the patch from last
4976 # upload, hence counted toward the "last upload" side in the final
4977 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004978 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004979 return 1
4980
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004981 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004982 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004983 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004984 if not issue:
4985 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004986 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004987 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004988
4989 # Create a new branch based on the merge-base
4990 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004991 # Clear cached branch in cl object, to avoid overwriting original CL branch
4992 # properties.
4993 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004994 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004995 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004996 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004997 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004998 return rtn
4999
wychen@chromium.org06928532015-02-03 02:11:29 +00005000 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005001 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005002 cmd = ['git', 'diff']
5003 if options.stat:
5004 cmd.append('--stat')
5005 cmd.extend([TMP_BRANCH, branch, '--'])
5006 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005007 finally:
5008 RunGit(['checkout', '-q', branch])
5009 RunGit(['branch', '-D', TMP_BRANCH])
5010
5011 return 0
5012
5013
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005014def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005015 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005016 parser.add_option(
5017 '--no-color',
5018 action='store_true',
5019 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005020 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005021 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005022 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005023
5024 author = RunGit(['config', 'user.email']).strip() or None
5025
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005026 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005027
5028 if args:
5029 if len(args) > 1:
5030 parser.error('Unknown args')
5031 base_branch = args[0]
5032 else:
5033 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005034 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005035
5036 change = cl.GetChange(base_branch, None)
5037 return owners_finder.OwnersFinder(
5038 [f.LocalPath() for f in
5039 cl.GetChange(base_branch, None).AffectedFiles()],
5040 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005041 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005042 disable_color=options.no_color).run()
5043
5044
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005045def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005046 """Generates a diff command."""
5047 # Generate diff for the current branch's changes.
5048 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005049 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005050
5051 if args:
5052 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005053 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005054 diff_cmd.append(arg)
5055 else:
5056 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005057
5058 return diff_cmd
5059
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005060
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005061def MatchingFileType(file_name, extensions):
5062 """Returns true if the file name ends with one of the given extensions."""
5063 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005064
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005065
enne@chromium.org555cfe42014-01-29 18:21:39 +00005066@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005067def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005068 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005069 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005070 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005071 parser.add_option('--full', action='store_true',
5072 help='Reformat the full content of all touched files')
5073 parser.add_option('--dry-run', action='store_true',
5074 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005075 parser.add_option('--python', action='store_true',
5076 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005077 parser.add_option('--js', action='store_true',
5078 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005079 parser.add_option('--diff', action='store_true',
5080 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005081 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005082
Daniel Chengc55eecf2016-12-30 03:11:02 -08005083 # Normalize any remaining args against the current path, so paths relative to
5084 # the current directory are still resolved as expected.
5085 args = [os.path.join(os.getcwd(), arg) for arg in args]
5086
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005087 # git diff generates paths against the root of the repository. Change
5088 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005089 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005090 if rel_base_path:
5091 os.chdir(rel_base_path)
5092
digit@chromium.org29e47272013-05-17 17:01:46 +00005093 # Grab the merge-base commit, i.e. the upstream commit of the current
5094 # branch when it was created or the last time it was rebased. This is
5095 # to cover the case where the user may have called "git fetch origin",
5096 # moving the origin branch to a newer commit, but hasn't rebased yet.
5097 upstream_commit = None
5098 cl = Changelist()
5099 upstream_branch = cl.GetUpstreamBranch()
5100 if upstream_branch:
5101 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5102 upstream_commit = upstream_commit.strip()
5103
5104 if not upstream_commit:
5105 DieWithError('Could not find base commit for this branch. '
5106 'Are you in detached state?')
5107
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005108 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5109 diff_output = RunGit(changed_files_cmd)
5110 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005111 # Filter out files deleted by this CL
5112 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005113
Christopher Lamc5ba6922017-01-24 11:19:14 +11005114 if opts.js:
5115 CLANG_EXTS.append('.js')
5116
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005117 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5118 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5119 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005120 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005121
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005122 top_dir = os.path.normpath(
5123 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5124
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005125 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5126 # formatted. This is used to block during the presubmit.
5127 return_value = 0
5128
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005129 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005130 # Locate the clang-format binary in the checkout
5131 try:
5132 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005133 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005134 DieWithError(e)
5135
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005136 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005137 cmd = [clang_format_tool]
5138 if not opts.dry_run and not opts.diff:
5139 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005140 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005141 if opts.diff:
5142 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005143 else:
5144 env = os.environ.copy()
5145 env['PATH'] = str(os.path.dirname(clang_format_tool))
5146 try:
5147 script = clang_format.FindClangFormatScriptInChromiumTree(
5148 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005149 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005150 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005151
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005152 cmd = [sys.executable, script, '-p0']
5153 if not opts.dry_run and not opts.diff:
5154 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005155
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005156 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5157 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005158
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005159 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5160 if opts.diff:
5161 sys.stdout.write(stdout)
5162 if opts.dry_run and len(stdout) > 0:
5163 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005164
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005165 # Similar code to above, but using yapf on .py files rather than clang-format
5166 # on C/C++ files
5167 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005168 yapf_tool = gclient_utils.FindExecutable('yapf')
5169 if yapf_tool is None:
5170 DieWithError('yapf not found in PATH')
5171
5172 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005173 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005174 cmd = [yapf_tool]
5175 if not opts.dry_run and not opts.diff:
5176 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005177 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005178 if opts.diff:
5179 sys.stdout.write(stdout)
5180 else:
5181 # TODO(sbc): yapf --lines mode still has some issues.
5182 # https://github.com/google/yapf/issues/154
5183 DieWithError('--python currently only works with --full')
5184
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005185 # Dart's formatter does not have the nice property of only operating on
5186 # modified chunks, so hard code full.
5187 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005188 try:
5189 command = [dart_format.FindDartFmtToolInChromiumTree()]
5190 if not opts.dry_run and not opts.diff:
5191 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005192 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005193
ppi@chromium.org6593d932016-03-03 15:41:15 +00005194 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005195 if opts.dry_run and stdout:
5196 return_value = 2
5197 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005198 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5199 'found in this checkout. Files in other languages are still '
5200 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005201
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005202 # Format GN build files. Always run on full build files for canonical form.
5203 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005204 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005205 if opts.dry_run or opts.diff:
5206 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005207 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005208 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5209 shell=sys.platform == 'win32',
5210 cwd=top_dir)
5211 if opts.dry_run and gn_ret == 2:
5212 return_value = 2 # Not formatted.
5213 elif opts.diff and gn_ret == 2:
5214 # TODO this should compute and print the actual diff.
5215 print("This change has GN build file diff for " + gn_diff_file)
5216 elif gn_ret != 0:
5217 # For non-dry run cases (and non-2 return values for dry-run), a
5218 # nonzero error code indicates a failure, probably because the file
5219 # doesn't parse.
5220 DieWithError("gn format failed on " + gn_diff_file +
5221 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005222
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005223 metrics_xml_files = [
5224 'tools/metrics/actions/actions.xml',
5225 'tools/metrics/histograms/histograms.xml',
5226 'tools/metrics/rappor/rappor.xml']
5227 for xml_file in metrics_xml_files:
5228 if xml_file in diff_files:
5229 tool_dir = top_dir + '/' + os.path.dirname(xml_file)
5230 cmd = [tool_dir + '/pretty_print.py', '--non-interactive']
5231 if opts.dry_run or opts.diff:
5232 cmd.append('--diff')
5233 stdout = RunCommand(cmd, cwd=top_dir)
5234 if opts.diff:
5235 sys.stdout.write(stdout)
5236 if opts.dry_run and stdout:
5237 return_value = 2 # Not formatted.
5238
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005239 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005240
5241
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005242@subcommand.usage('<codereview url or issue id>')
5243def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005244 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005245 _, args = parser.parse_args(args)
5246
5247 if len(args) != 1:
5248 parser.print_help()
5249 return 1
5250
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005251 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005252 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005253 parser.print_help()
5254 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005255 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005256
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005257 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005258 output = RunGit(['config', '--local', '--get-regexp',
5259 r'branch\..*\.%s' % issueprefix],
5260 error_ok=True)
5261 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005262 if issue == target_issue:
5263 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005264
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005265 branches = []
5266 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005267 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005268 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005269 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005270 return 1
5271 if len(branches) == 1:
5272 RunGit(['checkout', branches[0]])
5273 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005274 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005275 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005276 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005277 which = raw_input('Choose by index: ')
5278 try:
5279 RunGit(['checkout', branches[int(which)]])
5280 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005281 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005282 return 1
5283
5284 return 0
5285
5286
maruel@chromium.org29404b52014-09-08 22:58:00 +00005287def CMDlol(parser, args):
5288 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005289 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005290 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5291 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5292 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005293 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005294 return 0
5295
5296
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005297class OptionParser(optparse.OptionParser):
5298 """Creates the option parse and add --verbose support."""
5299 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005300 optparse.OptionParser.__init__(
5301 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005302 self.add_option(
5303 '-v', '--verbose', action='count', default=0,
5304 help='Use 2 times for more debugging info')
5305
5306 def parse_args(self, args=None, values=None):
5307 options, args = optparse.OptionParser.parse_args(self, args, values)
5308 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005309 logging.basicConfig(
5310 level=levels[min(options.verbose, len(levels) - 1)],
5311 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5312 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005313 return options, args
5314
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005315
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005316def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005317 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005318 print('\nYour python version %s is unsupported, please upgrade.\n' %
5319 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005320 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005321
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005322 # Reload settings.
5323 global settings
5324 settings = Settings()
5325
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005326 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005327 dispatcher = subcommand.CommandDispatcher(__name__)
5328 try:
5329 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005330 except auth.AuthenticationError as e:
5331 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005332 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005333 if e.code != 500:
5334 raise
5335 DieWithError(
5336 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5337 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005338 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005339
5340
5341if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005342 # These affect sys.stdout so do it outside of main() to simplify mocks in
5343 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005344 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005345 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005346 try:
5347 sys.exit(main(sys.argv[1:]))
5348 except KeyboardInterrupt:
5349 sys.stderr.write('interrupted\n')
5350 sys.exit(1)