blob: ef7fb82eb0c21b2b84cad81a4ddfe3a9cfabe374 [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
sheyang@google.com6ebaf782015-05-12 19:17:54 +000028import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000031import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
36try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080037 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038except ImportError:
39 pass
40
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000041from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000042from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000044import auth
skobes6468b902016-10-24 08:45:10 -070045import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000046import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000047import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000048import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000049import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000050import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000051import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000052import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000053import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000055import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000056import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000057import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000059import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import watchlists
64
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
tandrii9d2c7a32016-06-22 03:42:45 -070067COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070068DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080069POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000071REFS_THAT_ALIAS_TO_OTHER_REFS = {
72 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
73 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
74}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
thestig@chromium.org44202a22014-03-11 19:22:18 +000076# Valid extensions for files we want to lint.
77DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
78DEFAULT_LINT_IGNORE_REGEX = r"$^"
79
borenet6c0efe62016-10-19 08:13:29 -070080# Buildbucket master name prefix.
81MASTER_PREFIX = 'master.'
82
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000083# Shortcut since it quickly becomes redundant.
84Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000085
maruel@chromium.orgddd59412011-11-30 14:20:38 +000086# Initialized in main()
87settings = None
88
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010089# Used by tests/git_cl_test.py to add extra logging.
90# Inside the weirdly failing test, add this:
91# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
92# And scroll up to see the strack trace printed.
93_IS_BEING_TESTED = False
94
maruel@chromium.orgddd59412011-11-30 14:20:38 +000095
Christopher Lamf732cd52017-01-24 12:40:11 +110096def DieWithError(message, change_desc=None):
97 if change_desc:
98 SaveDescriptionBackup(change_desc)
99
vapiera7fbd5a2016-06-16 09:17:49 -0700100 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 sys.exit(1)
102
103
Christopher Lamf732cd52017-01-24 12:40:11 +1100104def SaveDescriptionBackup(change_desc):
105 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
106 print('\nError after CL description prompt -- saving description to %s\n' %
107 backup_path)
108 backup_file = open(backup_path, 'w')
109 backup_file.write(change_desc.description)
110 backup_file.close()
111
112
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000113def GetNoGitPagerEnv():
114 env = os.environ.copy()
115 # 'cat' is a magical git string that disables pagers on all platforms.
116 env['GIT_PAGER'] = 'cat'
117 return env
118
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000119
bsep@chromium.org627d9002016-04-29 00:00:52 +0000120def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000121 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000122 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000123 except subprocess2.CalledProcessError as e:
124 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000125 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000126 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000127 'Command "%s" failed.\n%s' % (
128 ' '.join(args), error_message or e.stdout or ''))
129 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
132def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000133 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000134 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000135
136
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000137def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000138 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700139 if suppress_stderr:
140 stderr = subprocess2.VOID
141 else:
142 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000143 try:
tandrii5d48c322016-08-18 16:19:37 -0700144 (out, _), code = subprocess2.communicate(['git'] + args,
145 env=GetNoGitPagerEnv(),
146 stdout=subprocess2.PIPE,
147 stderr=stderr)
148 return code, out
149 except subprocess2.CalledProcessError as e:
150 logging.debug('Failed running %s', args)
151 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000152
153
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000154def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000155 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000156 return RunGitWithCode(args, suppress_stderr=True)[1]
157
158
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000159def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000160 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000161 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000162 return (version.startswith(prefix) and
163 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000164
165
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000166def BranchExists(branch):
167 """Return True if specified branch exists."""
168 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
169 suppress_stderr=True)
170 return not code
171
172
tandrii2a16b952016-10-19 07:09:44 -0700173def time_sleep(seconds):
174 # Use this so that it can be mocked in tests without interfering with python
175 # system machinery.
176 import time # Local import to discourage others from importing time globally.
177 return time.sleep(seconds)
178
179
maruel@chromium.org90541732011-04-01 17:54:18 +0000180def ask_for_data(prompt):
181 try:
182 return raw_input(prompt)
183 except KeyboardInterrupt:
184 # Hide the exception.
185 sys.exit(1)
186
187
tandrii5d48c322016-08-18 16:19:37 -0700188def _git_branch_config_key(branch, key):
189 """Helper method to return Git config key for a branch."""
190 assert branch, 'branch name is required to set git config for it'
191 return 'branch.%s.%s' % (branch, key)
192
193
194def _git_get_branch_config_value(key, default=None, value_type=str,
195 branch=False):
196 """Returns git config value of given or current branch if any.
197
198 Returns default in all other cases.
199 """
200 assert value_type in (int, str, bool)
201 if branch is False: # Distinguishing default arg value from None.
202 branch = GetCurrentBranch()
203
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000204 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700205 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000206
tandrii5d48c322016-08-18 16:19:37 -0700207 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700208 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700209 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700210 # git config also has --int, but apparently git config suffers from integer
211 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700212 args.append(_git_branch_config_key(branch, key))
213 code, out = RunGitWithCode(args)
214 if code == 0:
215 value = out.strip()
216 if value_type == int:
217 return int(value)
218 if value_type == bool:
219 return bool(value.lower() == 'true')
220 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000221 return default
222
223
tandrii5d48c322016-08-18 16:19:37 -0700224def _git_set_branch_config_value(key, value, branch=None, **kwargs):
225 """Sets the value or unsets if it's None of a git branch config.
226
227 Valid, though not necessarily existing, branch must be provided,
228 otherwise currently checked out branch is used.
229 """
230 if not branch:
231 branch = GetCurrentBranch()
232 assert branch, 'a branch name OR currently checked out branch is required'
233 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700234 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700235 if value is None:
236 args.append('--unset')
237 elif isinstance(value, bool):
238 args.append('--bool')
239 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700240 else:
tandrii33a46ff2016-08-23 05:53:40 -0700241 # git config also has --int, but apparently git config suffers from integer
242 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700243 value = str(value)
244 args.append(_git_branch_config_key(branch, key))
245 if value is not None:
246 args.append(value)
247 RunGit(args, **kwargs)
248
249
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100250def _get_committer_timestamp(commit):
251 """Returns unix timestamp as integer of a committer in a commit.
252
253 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
254 """
255 # Git also stores timezone offset, but it only affects visual display,
256 # actual point in time is defined by this timestamp only.
257 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
258
259
260def _git_amend_head(message, committer_timestamp):
261 """Amends commit with new message and desired committer_timestamp.
262
263 Sets committer timezone to UTC.
264 """
265 env = os.environ.copy()
266 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
267 return RunGit(['commit', '--amend', '-m', message], env=env)
268
269
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000270def add_git_similarity(parser):
271 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700272 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000273 help='Sets the percentage that a pair of files need to match in order to'
274 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000275 parser.add_option(
276 '--find-copies', action='store_true',
277 help='Allows git to look for copies.')
278 parser.add_option(
279 '--no-find-copies', action='store_false', dest='find_copies',
280 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000281
282 old_parser_args = parser.parse_args
283 def Parse(args):
284 options, args = old_parser_args(args)
285
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000286 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700287 options.similarity = _git_get_branch_config_value(
288 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000289 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000290 print('Note: Saving similarity of %d%% in git config.'
291 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700292 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000293
iannucci@chromium.org79540052012-10-19 23:15:26 +0000294 options.similarity = max(0, min(options.similarity, 100))
295
296 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700297 options.find_copies = _git_get_branch_config_value(
298 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000299 else:
tandrii5d48c322016-08-18 16:19:37 -0700300 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000301
302 print('Using %d%% similarity for rename/copy detection. '
303 'Override with --similarity.' % options.similarity)
304
305 return options, args
306 parser.parse_args = Parse
307
308
machenbach@chromium.org45453142015-09-15 08:45:22 +0000309def _get_properties_from_options(options):
310 properties = dict(x.split('=', 1) for x in options.properties)
311 for key, val in properties.iteritems():
312 try:
313 properties[key] = json.loads(val)
314 except ValueError:
315 pass # If a value couldn't be evaluated, treat it as a string.
316 return properties
317
318
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000319def _prefix_master(master):
320 """Convert user-specified master name to full master name.
321
322 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
323 name, while the developers always use shortened master name
324 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
325 function does the conversion for buildbucket migration.
326 """
borenet6c0efe62016-10-19 08:13:29 -0700327 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000328 return master
borenet6c0efe62016-10-19 08:13:29 -0700329 return '%s%s' % (MASTER_PREFIX, master)
330
331
332def _unprefix_master(bucket):
333 """Convert bucket name to shortened master name.
334
335 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
336 name, while the developers always use shortened master name
337 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
338 function does the conversion for buildbucket migration.
339 """
340 if bucket.startswith(MASTER_PREFIX):
341 return bucket[len(MASTER_PREFIX):]
342 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000343
344
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000345def _buildbucket_retry(operation_name, http, *args, **kwargs):
346 """Retries requests to buildbucket service and returns parsed json content."""
347 try_count = 0
348 while True:
349 response, content = http.request(*args, **kwargs)
350 try:
351 content_json = json.loads(content)
352 except ValueError:
353 content_json = None
354
355 # Buildbucket could return an error even if status==200.
356 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000357 error = content_json.get('error')
358 if error.get('code') == 403:
359 raise BuildbucketResponseException(
360 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000361 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000362 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000363 raise BuildbucketResponseException(msg)
364
365 if response.status == 200:
366 if not content_json:
367 raise BuildbucketResponseException(
368 'Buildbucket returns invalid json content: %s.\n'
369 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
370 content)
371 return content_json
372 if response.status < 500 or try_count >= 2:
373 raise httplib2.HttpLib2Error(content)
374
375 # status >= 500 means transient failures.
376 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700377 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000378 try_count += 1
379 assert False, 'unreachable'
380
381
qyearsley1fdfcb62016-10-24 13:22:03 -0700382def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700383 """Returns a dict mapping bucket names to builders and tests,
384 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700385 """
qyearsleydd49f942016-10-28 11:57:22 -0700386 # If no bots are listed, we try to get a set of builders and tests based
387 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700388 if not options.bot:
389 change = changelist.GetChange(
390 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700391 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700392 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700393 change=change,
394 changed_files=change.LocalPaths(),
395 repository_root=settings.GetRoot(),
396 default_presubmit=None,
397 project=None,
398 verbose=options.verbose,
399 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700400 if masters is None:
401 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100402 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700403
qyearsley1fdfcb62016-10-24 13:22:03 -0700404 if options.bucket:
405 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700406 if options.master:
407 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700408
qyearsleydd49f942016-10-28 11:57:22 -0700409 # If bots are listed but no master or bucket, then we need to find out
410 # the corresponding master for each bot.
411 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
412 if error_message:
413 option_parser.error(
414 'Tryserver master cannot be found because: %s\n'
415 'Please manually specify the tryserver master, e.g. '
416 '"-m tryserver.chromium.linux".' % error_message)
417 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700418
419
qyearsley123a4682016-10-26 09:12:17 -0700420def _get_bucket_map_for_builders(builders):
421 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700422 map_url = 'https://builders-map.appspot.com/'
423 try:
qyearsley123a4682016-10-26 09:12:17 -0700424 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700425 except urllib2.URLError as e:
426 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
427 (map_url, e))
428 except ValueError as e:
429 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700430 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700431 return None, 'Failed to build master map.'
432
qyearsley123a4682016-10-26 09:12:17 -0700433 bucket_map = {}
434 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700435 masters = builders_map.get(builder, [])
436 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700437 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700438 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700439 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700440 (builder, masters))
441 bucket = _prefix_master(masters[0])
442 bucket_map.setdefault(bucket, {})[builder] = []
443
444 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700445
446
borenet6c0efe62016-10-19 08:13:29 -0700447def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700448 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700449 """Sends a request to Buildbucket to trigger try jobs for a changelist.
450
451 Args:
452 auth_config: AuthConfig for Rietveld.
453 changelist: Changelist that the try jobs are associated with.
454 buckets: A nested dict mapping bucket names to builders to tests.
455 options: Command-line options.
456 """
tandriide281ae2016-10-12 06:02:30 -0700457 assert changelist.GetIssue(), 'CL must be uploaded first'
458 codereview_url = changelist.GetCodereviewServer()
459 assert codereview_url, 'CL must be uploaded first'
460 patchset = patchset or changelist.GetMostRecentPatchset()
461 assert patchset, 'CL must be uploaded first'
462
463 codereview_host = urlparse.urlparse(codereview_url).hostname
464 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000465 http = authenticator.authorize(httplib2.Http())
466 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700467
468 # TODO(tandrii): consider caching Gerrit CL details just like
469 # _RietveldChangelistImpl does, then caching values in these two variables
470 # won't be necessary.
471 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000472
473 buildbucket_put_url = (
474 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000475 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700476 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
477 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
478 hostname=codereview_host,
479 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000480 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700481
482 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
483 shared_parameters_properties['category'] = category
484 if options.clobber:
485 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700486 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700487 if extra_properties:
488 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000489
490 batch_req_body = {'builds': []}
491 print_text = []
492 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700493 for bucket, builders_and_tests in sorted(buckets.iteritems()):
494 print_text.append('Bucket: %s' % bucket)
495 master = None
496 if bucket.startswith(MASTER_PREFIX):
497 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000498 for builder, tests in sorted(builders_and_tests.iteritems()):
499 print_text.append(' %s: %s' % (builder, tests))
500 parameters = {
501 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000502 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700503 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000504 'revision': options.revision,
505 }],
tandrii8c5a3532016-11-04 07:52:02 -0700506 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000507 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000508 if 'presubmit' in builder.lower():
509 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000510 if tests:
511 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700512
513 tags = [
514 'builder:%s' % builder,
515 'buildset:%s' % buildset,
516 'user_agent:git_cl_try',
517 ]
518 if master:
519 parameters['properties']['master'] = master
520 tags.append('master:%s' % master)
521
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000522 batch_req_body['builds'].append(
523 {
524 'bucket': bucket,
525 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000526 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700527 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000528 }
529 )
530
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000531 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700532 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000533 http,
534 buildbucket_put_url,
535 'PUT',
536 body=json.dumps(batch_req_body),
537 headers={'Content-Type': 'application/json'}
538 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000539 print_text.append('To see results here, run: git cl try-results')
540 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700541 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000542
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000543
tandrii221ab252016-10-06 08:12:04 -0700544def fetch_try_jobs(auth_config, changelist, buildbucket_host,
545 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700546 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000547
qyearsley53f48a12016-09-01 10:45:13 -0700548 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000549 """
tandrii221ab252016-10-06 08:12:04 -0700550 assert buildbucket_host
551 assert changelist.GetIssue(), 'CL must be uploaded first'
552 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
553 patchset = patchset or changelist.GetMostRecentPatchset()
554 assert patchset, 'CL must be uploaded first'
555
556 codereview_url = changelist.GetCodereviewServer()
557 codereview_host = urlparse.urlparse(codereview_url).hostname
558 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 if authenticator.has_cached_credentials():
560 http = authenticator.authorize(httplib2.Http())
561 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700562 print('Warning: Some results might be missing because %s' %
563 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700564 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565 http = httplib2.Http()
566
567 http.force_exception_to_status_code = True
568
tandrii221ab252016-10-06 08:12:04 -0700569 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
570 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
571 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700573 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000574 params = {'tag': 'buildset:%s' % buildset}
575
576 builds = {}
577 while True:
578 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700579 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000580 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700581 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000582 for build in content.get('builds', []):
583 builds[build['id']] = build
584 if 'next_cursor' in content:
585 params['start_cursor'] = content['next_cursor']
586 else:
587 break
588 return builds
589
590
qyearsleyeab3c042016-08-24 09:18:28 -0700591def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000592 """Prints nicely result of fetch_try_jobs."""
593 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700594 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000595 return
596
597 # Make a copy, because we'll be modifying builds dictionary.
598 builds = builds.copy()
599 builder_names_cache = {}
600
601 def get_builder(b):
602 try:
603 return builder_names_cache[b['id']]
604 except KeyError:
605 try:
606 parameters = json.loads(b['parameters_json'])
607 name = parameters['builder_name']
608 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700609 print('WARNING: failed to get builder name for build %s: %s' % (
610 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000611 name = None
612 builder_names_cache[b['id']] = name
613 return name
614
615 def get_bucket(b):
616 bucket = b['bucket']
617 if bucket.startswith('master.'):
618 return bucket[len('master.'):]
619 return bucket
620
621 if options.print_master:
622 name_fmt = '%%-%ds %%-%ds' % (
623 max(len(str(get_bucket(b))) for b in builds.itervalues()),
624 max(len(str(get_builder(b))) for b in builds.itervalues()))
625 def get_name(b):
626 return name_fmt % (get_bucket(b), get_builder(b))
627 else:
628 name_fmt = '%%-%ds' % (
629 max(len(str(get_builder(b))) for b in builds.itervalues()))
630 def get_name(b):
631 return name_fmt % get_builder(b)
632
633 def sort_key(b):
634 return b['status'], b.get('result'), get_name(b), b.get('url')
635
636 def pop(title, f, color=None, **kwargs):
637 """Pop matching builds from `builds` dict and print them."""
638
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000639 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000640 colorize = str
641 else:
642 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
643
644 result = []
645 for b in builds.values():
646 if all(b.get(k) == v for k, v in kwargs.iteritems()):
647 builds.pop(b['id'])
648 result.append(b)
649 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700650 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000651 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700652 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000653
654 total = len(builds)
655 pop(status='COMPLETED', result='SUCCESS',
656 title='Successes:', color=Fore.GREEN,
657 f=lambda b: (get_name(b), b.get('url')))
658 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
659 title='Infra Failures:', color=Fore.MAGENTA,
660 f=lambda b: (get_name(b), b.get('url')))
661 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
662 title='Failures:', color=Fore.RED,
663 f=lambda b: (get_name(b), b.get('url')))
664 pop(status='COMPLETED', result='CANCELED',
665 title='Canceled:', color=Fore.MAGENTA,
666 f=lambda b: (get_name(b),))
667 pop(status='COMPLETED', result='FAILURE',
668 failure_reason='INVALID_BUILD_DEFINITION',
669 title='Wrong master/builder name:', color=Fore.MAGENTA,
670 f=lambda b: (get_name(b),))
671 pop(status='COMPLETED', result='FAILURE',
672 title='Other failures:',
673 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
674 pop(status='COMPLETED',
675 title='Other finished:',
676 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
677 pop(status='STARTED',
678 title='Started:', color=Fore.YELLOW,
679 f=lambda b: (get_name(b), b.get('url')))
680 pop(status='SCHEDULED',
681 title='Scheduled:',
682 f=lambda b: (get_name(b), 'id=%s' % b['id']))
683 # The last section is just in case buildbucket API changes OR there is a bug.
684 pop(title='Other:',
685 f=lambda b: (get_name(b), 'id=%s' % b['id']))
686 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700687 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000688
689
qyearsley53f48a12016-09-01 10:45:13 -0700690def write_try_results_json(output_file, builds):
691 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
692
693 The input |builds| dict is assumed to be generated by Buildbucket.
694 Buildbucket documentation: http://goo.gl/G0s101
695 """
696
697 def convert_build_dict(build):
698 return {
699 'buildbucket_id': build.get('id'),
700 'status': build.get('status'),
701 'result': build.get('result'),
702 'bucket': build.get('bucket'),
703 'builder_name': json.loads(
704 build.get('parameters_json', '{}')).get('builder_name'),
705 'failure_reason': build.get('failure_reason'),
706 'url': build.get('url'),
707 }
708
709 converted = []
710 for _, build in sorted(builds.items()):
711 converted.append(convert_build_dict(build))
712 write_json(output_file, converted)
713
714
iannucci@chromium.org79540052012-10-19 23:15:26 +0000715def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000716 """Prints statistics about the change to the user."""
717 # --no-ext-diff is broken in some versions of Git, so try to work around
718 # this by overriding the environment (but there is still a problem if the
719 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000720 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000721 if 'GIT_EXTERNAL_DIFF' in env:
722 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000723
724 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800725 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000726 else:
727 similarity_options = ['-M%s' % similarity]
728
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000729 try:
730 stdout = sys.stdout.fileno()
731 except AttributeError:
732 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000733 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000734 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000735 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000736 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000737
738
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000739class BuildbucketResponseException(Exception):
740 pass
741
742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743class Settings(object):
744 def __init__(self):
745 self.default_server = None
746 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000747 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000748 self.tree_status_url = None
749 self.viewvc_url = None
750 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000751 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000752 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000753 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000754 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000755 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000756 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757
758 def LazyUpdateIfNeeded(self):
759 """Updates the settings from a codereview.settings file, if available."""
760 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000761 # The only value that actually changes the behavior is
762 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000763 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000764 error_ok=True
765 ).strip().lower()
766
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000768 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000769 LoadCodereviewSettingsFromFile(cr_settings_file)
770 self.updated = True
771
772 def GetDefaultServerUrl(self, error_ok=False):
773 if not self.default_server:
774 self.LazyUpdateIfNeeded()
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_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777 if error_ok:
778 return self.default_server
779 if not self.default_server:
780 error_message = ('Could not find settings file. You must configure '
781 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000782 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000783 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784 return self.default_server
785
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000786 @staticmethod
787 def GetRelativeRoot():
788 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000789
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000791 if self.root is None:
792 self.root = os.path.abspath(self.GetRelativeRoot())
793 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000795 def GetGitMirror(self, remote='origin'):
796 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000797 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000798 if not os.path.isdir(local_url):
799 return None
800 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
801 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100802 # Use the /dev/null print_func to avoid terminal spew.
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000803 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
804 if mirror.exists():
805 return mirror
806 return None
807
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808 def GetTreeStatusUrl(self, error_ok=False):
809 if not self.tree_status_url:
810 error_message = ('You must configure your tree status URL by running '
811 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000812 self.tree_status_url = self._GetRietveldConfig(
813 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 return self.tree_status_url
815
816 def GetViewVCUrl(self):
817 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000818 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819 return self.viewvc_url
820
rmistry@google.com90752582014-01-14 21:04:50 +0000821 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000822 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000823
rmistry@google.com78948ed2015-07-08 23:09:57 +0000824 def GetIsSkipDependencyUpload(self, branch_name):
825 """Returns true if specified branch should skip dep uploads."""
826 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
827 error_ok=True)
828
rmistry@google.com5626a922015-02-26 14:03:30 +0000829 def GetRunPostUploadHook(self):
830 run_post_upload_hook = self._GetRietveldConfig(
831 'run-post-upload-hook', error_ok=True)
832 return run_post_upload_hook == "True"
833
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000834 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000835 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000836
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000837 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000838 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000839
ukai@chromium.orge8077812012-02-03 03:41:46 +0000840 def GetIsGerrit(self):
841 """Return true if this repo is assosiated with gerrit code review system."""
842 if self.is_gerrit is None:
843 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
844 return self.is_gerrit
845
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000846 def GetSquashGerritUploads(self):
847 """Return true if uploads to Gerrit should be squashed by default."""
848 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700849 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
850 if self.squash_gerrit_uploads is None:
851 # Default is squash now (http://crbug.com/611892#c23).
852 self.squash_gerrit_uploads = not (
853 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
854 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000855 return self.squash_gerrit_uploads
856
tandriia60502f2016-06-20 02:01:53 -0700857 def GetSquashGerritUploadsOverride(self):
858 """Return True or False if codereview.settings should be overridden.
859
860 Returns None if no override has been defined.
861 """
862 # See also http://crbug.com/611892#c23
863 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
864 error_ok=True).strip()
865 if result == 'true':
866 return True
867 if result == 'false':
868 return False
869 return None
870
tandrii@chromium.org28253532016-04-14 13:46:56 +0000871 def GetGerritSkipEnsureAuthenticated(self):
872 """Return True if EnsureAuthenticated should not be done for Gerrit
873 uploads."""
874 if self.gerrit_skip_ensure_authenticated is None:
875 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000876 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000877 error_ok=True).strip() == 'true')
878 return self.gerrit_skip_ensure_authenticated
879
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000880 def GetGitEditor(self):
881 """Return the editor specified in the git config, or None if none is."""
882 if self.git_editor is None:
883 self.git_editor = self._GetConfig('core.editor', error_ok=True)
884 return self.git_editor or None
885
thestig@chromium.org44202a22014-03-11 19:22:18 +0000886 def GetLintRegex(self):
887 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
888 DEFAULT_LINT_REGEX)
889
890 def GetLintIgnoreRegex(self):
891 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
892 DEFAULT_LINT_IGNORE_REGEX)
893
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000894 def GetProject(self):
895 if not self.project:
896 self.project = self._GetRietveldConfig('project', error_ok=True)
897 return self.project
898
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000899 def _GetRietveldConfig(self, param, **kwargs):
900 return self._GetConfig('rietveld.' + param, **kwargs)
901
rmistry@google.com78948ed2015-07-08 23:09:57 +0000902 def _GetBranchConfig(self, branch_name, param, **kwargs):
903 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 def _GetConfig(self, param, **kwargs):
906 self.LazyUpdateIfNeeded()
907 return RunGit(['config', param], **kwargs).strip()
908
909
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100910@contextlib.contextmanager
911def _get_gerrit_project_config_file(remote_url):
912 """Context manager to fetch and store Gerrit's project.config from
913 refs/meta/config branch and store it in temp file.
914
915 Provides a temporary filename or None if there was error.
916 """
917 error, _ = RunGitWithCode([
918 'fetch', remote_url,
919 '+refs/meta/config:refs/git_cl/meta/config'])
920 if error:
921 # Ref doesn't exist or isn't accessible to current user.
922 print('WARNING: failed to fetch project config for %s: %s' %
923 (remote_url, error))
924 yield None
925 return
926
927 error, project_config_data = RunGitWithCode(
928 ['show', 'refs/git_cl/meta/config:project.config'])
929 if error:
930 print('WARNING: project.config file not found')
931 yield None
932 return
933
934 with gclient_utils.temporary_directory() as tempdir:
935 project_config_file = os.path.join(tempdir, 'project.config')
936 gclient_utils.FileWrite(project_config_file, project_config_data)
937 yield project_config_file
938
939
940def _is_git_numberer_enabled(remote_url, remote_ref):
941 """Returns True if Git Numberer is enabled on this ref."""
942 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100943 KNOWN_PROJECTS_WHITELIST = [
944 'chromium/src',
945 'external/webrtc',
946 'v8/v8',
947 ]
948
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100949 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
950 url_parts = urlparse.urlparse(remote_url)
951 project_name = url_parts.path.lstrip('/').rstrip('git./')
952 for known in KNOWN_PROJECTS_WHITELIST:
953 if project_name.endswith(known):
954 break
955 else:
956 # Early exit to avoid extra fetches for repos that aren't using Git
957 # Numberer.
958 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100959
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100960 with _get_gerrit_project_config_file(remote_url) as project_config_file:
961 if project_config_file is None:
962 # Failed to fetch project.config, which shouldn't happen on open source
963 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100964 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100965 def get_opts(x):
966 code, out = RunGitWithCode(
967 ['config', '-f', project_config_file, '--get-all',
968 'plugin.git-numberer.validate-%s-refglob' % x])
969 if code == 0:
970 return out.strip().splitlines()
971 return []
972 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100973
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100974 logging.info('validator config enabled %s disabled %s refglobs for '
975 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000976
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100977 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100978 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100979 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100980 return True
981 return False
982
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100983 if match_refglobs(disabled):
984 return False
985 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100986
987
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000988def ShortBranchName(branch):
989 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000990 return branch.replace('refs/heads/', '', 1)
991
992
993def GetCurrentBranchRef():
994 """Returns branch ref (e.g., refs/heads/master) or None."""
995 return RunGit(['symbolic-ref', 'HEAD'],
996 stderr=subprocess2.VOID, error_ok=True).strip() or None
997
998
999def GetCurrentBranch():
1000 """Returns current branch or None.
1001
1002 For refs/heads/* branches, returns just last part. For others, full ref.
1003 """
1004 branchref = GetCurrentBranchRef()
1005 if branchref:
1006 return ShortBranchName(branchref)
1007 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008
1009
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001010class _CQState(object):
1011 """Enum for states of CL with respect to Commit Queue."""
1012 NONE = 'none'
1013 DRY_RUN = 'dry_run'
1014 COMMIT = 'commit'
1015
1016 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1017
1018
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001019class _ParsedIssueNumberArgument(object):
1020 def __init__(self, issue=None, patchset=None, hostname=None):
1021 self.issue = issue
1022 self.patchset = patchset
1023 self.hostname = hostname
1024
1025 @property
1026 def valid(self):
1027 return self.issue is not None
1028
1029
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001030def ParseIssueNumberArgument(arg):
1031 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1032 fail_result = _ParsedIssueNumberArgument()
1033
1034 if arg.isdigit():
1035 return _ParsedIssueNumberArgument(issue=int(arg))
1036 if not arg.startswith('http'):
1037 return fail_result
1038 url = gclient_utils.UpgradeToHttps(arg)
1039 try:
1040 parsed_url = urlparse.urlparse(url)
1041 except ValueError:
1042 return fail_result
1043 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1044 tmp = cls.ParseIssueURL(parsed_url)
1045 if tmp is not None:
1046 return tmp
1047 return fail_result
1048
1049
Aaron Gablea45ee112016-11-22 15:14:38 -08001050class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001051 def __init__(self, issue, url):
1052 self.issue = issue
1053 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001054 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001055
1056 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001057 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001058 self.issue, self.url)
1059
1060
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001062 """Changelist works with one changelist in local branch.
1063
1064 Supports two codereview backends: Rietveld or Gerrit, selected at object
1065 creation.
1066
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001067 Notes:
1068 * Not safe for concurrent multi-{thread,process} use.
1069 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001070 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001071 """
1072
1073 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1074 """Create a new ChangeList instance.
1075
1076 If issue is given, the codereview must be given too.
1077
1078 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1079 Otherwise, it's decided based on current configuration of the local branch,
1080 with default being 'rietveld' for backwards compatibility.
1081 See _load_codereview_impl for more details.
1082
1083 **kwargs will be passed directly to codereview implementation.
1084 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001085 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001086 global settings
1087 if not settings:
1088 # Happens when git_cl.py is used as a utility library.
1089 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001090
1091 if issue:
1092 assert codereview, 'codereview must be known, if issue is known'
1093
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094 self.branchref = branchref
1095 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001096 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097 self.branch = ShortBranchName(self.branchref)
1098 else:
1099 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001101 self.lookedup_issue = False
1102 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103 self.has_description = False
1104 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001105 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001107 self.cc = None
1108 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001109 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001110
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001111 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001112 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001113 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001114 assert self._codereview_impl
1115 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116
1117 def _load_codereview_impl(self, codereview=None, **kwargs):
1118 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001119 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1120 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1121 self._codereview = codereview
1122 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001123 return
1124
1125 # Automatic selection based on issue number set for a current branch.
1126 # Rietveld takes precedence over Gerrit.
1127 assert not self.issue
1128 # Whether we find issue or not, we are doing the lookup.
1129 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001130 if self.GetBranch():
1131 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1132 issue = _git_get_branch_config_value(
1133 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1134 if issue:
1135 self._codereview = codereview
1136 self._codereview_impl = cls(self, **kwargs)
1137 self.issue = int(issue)
1138 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001139
1140 # No issue is set for this branch, so decide based on repo-wide settings.
1141 return self._load_codereview_impl(
1142 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1143 **kwargs)
1144
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001145 def IsGerrit(self):
1146 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001147
1148 def GetCCList(self):
1149 """Return the users cc'd on this CL.
1150
agable92bec4f2016-08-24 09:27:27 -07001151 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001152 """
1153 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001154 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001155 more_cc = ','.join(self.watchers)
1156 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1157 return self.cc
1158
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001159 def GetCCListWithoutDefault(self):
1160 """Return the users cc'd on this CL excluding default ones."""
1161 if self.cc is None:
1162 self.cc = ','.join(self.watchers)
1163 return self.cc
1164
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001165 def SetWatchers(self, watchers):
1166 """Set the list of email addresses that should be cc'd based on the changed
1167 files in this CL.
1168 """
1169 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170
1171 def GetBranch(self):
1172 """Returns the short branch name, e.g. 'master'."""
1173 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001174 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001175 if not branchref:
1176 return None
1177 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 self.branch = ShortBranchName(self.branchref)
1179 return self.branch
1180
1181 def GetBranchRef(self):
1182 """Returns the full branch name, e.g. 'refs/heads/master'."""
1183 self.GetBranch() # Poke the lazy loader.
1184 return self.branchref
1185
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001186 def ClearBranch(self):
1187 """Clears cached branch data of this object."""
1188 self.branch = self.branchref = None
1189
tandrii5d48c322016-08-18 16:19:37 -07001190 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1191 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1192 kwargs['branch'] = self.GetBranch()
1193 return _git_get_branch_config_value(key, default, **kwargs)
1194
1195 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1196 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1197 assert self.GetBranch(), (
1198 'this CL must have an associated branch to %sset %s%s' %
1199 ('un' if value is None else '',
1200 key,
1201 '' if value is None else ' to %r' % value))
1202 kwargs['branch'] = self.GetBranch()
1203 return _git_set_branch_config_value(key, value, **kwargs)
1204
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001205 @staticmethod
1206 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001207 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 e.g. 'origin', 'refs/heads/master'
1209 """
1210 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001211 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1212
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001214 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001216 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1217 error_ok=True).strip()
1218 if upstream_branch:
1219 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001221 # Else, try to guess the origin remote.
1222 remote_branches = RunGit(['branch', '-r']).split()
1223 if 'origin/master' in remote_branches:
1224 # Fall back on origin/master if it exits.
1225 remote = 'origin'
1226 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001228 DieWithError(
1229 'Unable to determine default branch to diff against.\n'
1230 'Either pass complete "git diff"-style arguments, like\n'
1231 ' git cl upload origin/master\n'
1232 'or verify this branch is set up to track another \n'
1233 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234
1235 return remote, upstream_branch
1236
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001237 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001238 upstream_branch = self.GetUpstreamBranch()
1239 if not BranchExists(upstream_branch):
1240 DieWithError('The upstream for the current branch (%s) does not exist '
1241 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001242 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001243 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001244
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 def GetUpstreamBranch(self):
1246 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001247 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001249 upstream_branch = upstream_branch.replace('refs/heads/',
1250 'refs/remotes/%s/' % remote)
1251 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1252 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 self.upstream_branch = upstream_branch
1254 return self.upstream_branch
1255
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001256 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001257 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001258 remote, branch = None, self.GetBranch()
1259 seen_branches = set()
1260 while branch not in seen_branches:
1261 seen_branches.add(branch)
1262 remote, branch = self.FetchUpstreamTuple(branch)
1263 branch = ShortBranchName(branch)
1264 if remote != '.' or branch.startswith('refs/remotes'):
1265 break
1266 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001267 remotes = RunGit(['remote'], error_ok=True).split()
1268 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001269 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001270 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001271 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001272 logging.warn('Could not determine which remote this change is '
1273 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001274 else:
1275 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001276 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001277 branch = 'HEAD'
1278 if branch.startswith('refs/remotes'):
1279 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001280 elif branch.startswith('refs/branch-heads/'):
1281 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001282 else:
1283 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001284 return self._remote
1285
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001286 def GitSanityChecks(self, upstream_git_obj):
1287 """Checks git repo status and ensures diff is from local commits."""
1288
sbc@chromium.org79706062015-01-14 21:18:12 +00001289 if upstream_git_obj is None:
1290 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001291 print('ERROR: unable to determine current branch (detached HEAD?)',
1292 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001293 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001294 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001295 return False
1296
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001297 # Verify the commit we're diffing against is in our current branch.
1298 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1299 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1300 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001301 print('ERROR: %s is not in the current branch. You may need to rebase '
1302 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001303 return False
1304
1305 # List the commits inside the diff, and verify they are all local.
1306 commits_in_diff = RunGit(
1307 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1308 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1309 remote_branch = remote_branch.strip()
1310 if code != 0:
1311 _, remote_branch = self.GetRemoteBranch()
1312
1313 commits_in_remote = RunGit(
1314 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1315
1316 common_commits = set(commits_in_diff) & set(commits_in_remote)
1317 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001318 print('ERROR: Your diff contains %d commits already in %s.\n'
1319 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1320 'the diff. If you are using a custom git flow, you can override'
1321 ' the reference used for this check with "git config '
1322 'gitcl.remotebranch <git-ref>".' % (
1323 len(common_commits), remote_branch, upstream_git_obj),
1324 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001325 return False
1326 return True
1327
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001328 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001329 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001330
1331 Returns None if it is not set.
1332 """
tandrii5d48c322016-08-18 16:19:37 -07001333 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001334
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335 def GetRemoteUrl(self):
1336 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1337
1338 Returns None if there is no remote.
1339 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001340 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001341 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1342
1343 # If URL is pointing to a local directory, it is probably a git cache.
1344 if os.path.isdir(url):
1345 url = RunGit(['config', 'remote.%s.url' % remote],
1346 error_ok=True,
1347 cwd=url).strip()
1348 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001350 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001351 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001352 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001353 self.issue = self._GitGetBranchConfigValue(
1354 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001355 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 return self.issue
1357
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358 def GetIssueURL(self):
1359 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001360 issue = self.GetIssue()
1361 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001362 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001363 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001364
1365 def GetDescription(self, pretty=False):
1366 if not self.has_description:
1367 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001368 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 self.has_description = True
1370 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001371 # Set width to 72 columns + 2 space indent.
1372 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001374 lines = self.description.splitlines()
1375 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001376 return self.description
1377
1378 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001379 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001380 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001381 self.patchset = self._GitGetBranchConfigValue(
1382 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001383 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001384 return self.patchset
1385
1386 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001387 """Set this branch's patchset. If patchset=0, clears the patchset."""
1388 assert self.GetBranch()
1389 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001390 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001391 else:
1392 self.patchset = int(patchset)
1393 self._GitSetBranchConfigValue(
1394 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001396 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001397 """Set this branch's issue. If issue isn't given, clears the issue."""
1398 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001400 issue = int(issue)
1401 self._GitSetBranchConfigValue(
1402 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001403 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001404 codereview_server = self._codereview_impl.GetCodereviewServer()
1405 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001406 self._GitSetBranchConfigValue(
1407 self._codereview_impl.CodereviewServerConfigKey(),
1408 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409 else:
tandrii5d48c322016-08-18 16:19:37 -07001410 # Reset all of these just to be clean.
1411 reset_suffixes = [
1412 'last-upload-hash',
1413 self._codereview_impl.IssueConfigKey(),
1414 self._codereview_impl.PatchsetConfigKey(),
1415 self._codereview_impl.CodereviewServerConfigKey(),
1416 ] + self._PostUnsetIssueProperties()
1417 for prop in reset_suffixes:
1418 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001419 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001420 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421
dnjba1b0f32016-09-02 12:37:42 -07001422 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001423 if not self.GitSanityChecks(upstream_branch):
1424 DieWithError('\nGit sanity check failure')
1425
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001426 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001427 if not root:
1428 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001429 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001430
1431 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001432 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001433 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001434 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001435 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001436 except subprocess2.CalledProcessError:
1437 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001438 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001439 'This branch probably doesn\'t exist anymore. To reset the\n'
1440 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001441 ' git branch --set-upstream-to origin/master %s\n'
1442 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001443 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001444
maruel@chromium.org52424302012-08-29 15:14:30 +00001445 issue = self.GetIssue()
1446 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001447 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001448 description = self.GetDescription()
1449 else:
1450 # If the change was never uploaded, use the log messages of all commits
1451 # up to the branch point, as git cl upload will prefill the description
1452 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001453 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1454 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001455
1456 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001457 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001458 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001459 name,
1460 description,
1461 absroot,
1462 files,
1463 issue,
1464 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001465 author,
1466 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001467
dsansomee2d6fd92016-09-08 00:10:47 -07001468 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001469 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001470 return self._codereview_impl.UpdateDescriptionRemote(
1471 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001472
1473 def RunHook(self, committing, may_prompt, verbose, change):
1474 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1475 try:
1476 return presubmit_support.DoPresubmitChecks(change, committing,
1477 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1478 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001479 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1480 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001481 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001482 DieWithError(
1483 ('%s\nMaybe your depot_tools is out of date?\n'
1484 'If all fails, contact maruel@') % e)
1485
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001486 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1487 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001488 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1489 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001490 else:
1491 # Assume url.
1492 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1493 urlparse.urlparse(issue_arg))
1494 if not parsed_issue_arg or not parsed_issue_arg.valid:
1495 DieWithError('Failed to parse issue argument "%s". '
1496 'Must be an issue number or a valid URL.' % issue_arg)
1497 return self._codereview_impl.CMDPatchWithParsedIssue(
1498 parsed_issue_arg, reject, nocommit, directory)
1499
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001500 def CMDUpload(self, options, git_diff_args, orig_args):
1501 """Uploads a change to codereview."""
1502 if git_diff_args:
1503 # TODO(ukai): is it ok for gerrit case?
1504 base_branch = git_diff_args[0]
1505 else:
1506 if self.GetBranch() is None:
1507 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1508
1509 # Default to diffing against common ancestor of upstream branch
1510 base_branch = self.GetCommonAncestorWithUpstream()
1511 git_diff_args = [base_branch, 'HEAD']
1512
1513 # Make sure authenticated to codereview before running potentially expensive
1514 # hooks. It is a fast, best efforts check. Codereview still can reject the
1515 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001516 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001517
1518 # Apply watchlists on upload.
1519 change = self.GetChange(base_branch, None)
1520 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1521 files = [f.LocalPath() for f in change.AffectedFiles()]
1522 if not options.bypass_watchlists:
1523 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1524
1525 if not options.bypass_hooks:
1526 if options.reviewers or options.tbr_owners:
1527 # Set the reviewer list now so that presubmit checks can access it.
1528 change_description = ChangeDescription(change.FullDescriptionText())
1529 change_description.update_reviewers(options.reviewers,
1530 options.tbr_owners,
1531 change)
1532 change.SetDescriptionText(change_description.description)
1533 hook_results = self.RunHook(committing=False,
1534 may_prompt=not options.force,
1535 verbose=options.verbose,
1536 change=change)
1537 if not hook_results.should_continue():
1538 return 1
1539 if not options.reviewers and hook_results.reviewers:
1540 options.reviewers = hook_results.reviewers.split(',')
1541
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001542 # TODO(tandrii): Checking local patchset against remote patchset is only
1543 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1544 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001545 latest_patchset = self.GetMostRecentPatchset()
1546 local_patchset = self.GetPatchset()
1547 if (latest_patchset and local_patchset and
1548 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001549 print('The last upload made from this repository was patchset #%d but '
1550 'the most recent patchset on the server is #%d.'
1551 % (local_patchset, latest_patchset))
1552 print('Uploading will still work, but if you\'ve uploaded to this '
1553 'issue from another machine or branch the patch you\'re '
1554 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001555 ask_for_data('About to upload; enter to confirm.')
1556
1557 print_stats(options.similarity, options.find_copies, git_diff_args)
1558 ret = self.CMDUploadChange(options, git_diff_args, change)
1559 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001560 if options.use_commit_queue:
1561 self.SetCQState(_CQState.COMMIT)
1562 elif options.cq_dry_run:
1563 self.SetCQState(_CQState.DRY_RUN)
1564
tandrii5d48c322016-08-18 16:19:37 -07001565 _git_set_branch_config_value('last-upload-hash',
1566 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001567 # Run post upload hooks, if specified.
1568 if settings.GetRunPostUploadHook():
1569 presubmit_support.DoPostUploadExecuter(
1570 change,
1571 self,
1572 settings.GetRoot(),
1573 options.verbose,
1574 sys.stdout)
1575
1576 # Upload all dependencies if specified.
1577 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001578 print()
1579 print('--dependencies has been specified.')
1580 print('All dependent local branches will be re-uploaded.')
1581 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001582 # Remove the dependencies flag from args so that we do not end up in a
1583 # loop.
1584 orig_args.remove('--dependencies')
1585 ret = upload_branch_deps(self, orig_args)
1586 return ret
1587
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001588 def SetCQState(self, new_state):
1589 """Update the CQ state for latest patchset.
1590
1591 Issue must have been already uploaded and known.
1592 """
1593 assert new_state in _CQState.ALL_STATES
1594 assert self.GetIssue()
1595 return self._codereview_impl.SetCQState(new_state)
1596
qyearsley1fdfcb62016-10-24 13:22:03 -07001597 def TriggerDryRun(self):
1598 """Triggers a dry run and prints a warning on failure."""
1599 # TODO(qyearsley): Either re-use this method in CMDset_commit
1600 # and CMDupload, or change CMDtry to trigger dry runs with
1601 # just SetCQState, and catch keyboard interrupt and other
1602 # errors in that method.
1603 try:
1604 self.SetCQState(_CQState.DRY_RUN)
1605 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1606 return 0
1607 except KeyboardInterrupt:
1608 raise
1609 except:
1610 print('WARNING: failed to trigger CQ Dry Run.\n'
1611 'Either:\n'
1612 ' * your project has no CQ\n'
1613 ' * you don\'t have permission to trigger Dry Run\n'
1614 ' * bug in this code (see stack trace below).\n'
1615 'Consider specifying which bots to trigger manually '
1616 'or asking your project owners for permissions '
1617 'or contacting Chrome Infrastructure team at '
1618 'https://www.chromium.org/infra\n\n')
1619 # Still raise exception so that stack trace is printed.
1620 raise
1621
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001622 # Forward methods to codereview specific implementation.
1623
1624 def CloseIssue(self):
1625 return self._codereview_impl.CloseIssue()
1626
1627 def GetStatus(self):
1628 return self._codereview_impl.GetStatus()
1629
1630 def GetCodereviewServer(self):
1631 return self._codereview_impl.GetCodereviewServer()
1632
tandriide281ae2016-10-12 06:02:30 -07001633 def GetIssueOwner(self):
1634 """Get owner from codereview, which may differ from this checkout."""
1635 return self._codereview_impl.GetIssueOwner()
1636
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001637 def GetApprovingReviewers(self):
1638 return self._codereview_impl.GetApprovingReviewers()
1639
1640 def GetMostRecentPatchset(self):
1641 return self._codereview_impl.GetMostRecentPatchset()
1642
tandriide281ae2016-10-12 06:02:30 -07001643 def CannotTriggerTryJobReason(self):
1644 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1645 return self._codereview_impl.CannotTriggerTryJobReason()
1646
tandrii8c5a3532016-11-04 07:52:02 -07001647 def GetTryjobProperties(self, patchset=None):
1648 """Returns dictionary of properties to launch tryjob."""
1649 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1650
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001651 def __getattr__(self, attr):
1652 # This is because lots of untested code accesses Rietveld-specific stuff
1653 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001654 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001655 # Note that child method defines __getattr__ as well, and forwards it here,
1656 # because _RietveldChangelistImpl is not cleaned up yet, and given
1657 # deprecation of Rietveld, it should probably be just removed.
1658 # Until that time, avoid infinite recursion by bypassing __getattr__
1659 # of implementation class.
1660 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001661
1662
1663class _ChangelistCodereviewBase(object):
1664 """Abstract base class encapsulating codereview specifics of a changelist."""
1665 def __init__(self, changelist):
1666 self._changelist = changelist # instance of Changelist
1667
1668 def __getattr__(self, attr):
1669 # Forward methods to changelist.
1670 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1671 # _RietveldChangelistImpl to avoid this hack?
1672 return getattr(self._changelist, attr)
1673
1674 def GetStatus(self):
1675 """Apply a rough heuristic to give a simple summary of an issue's review
1676 or CQ status, assuming adherence to a common workflow.
1677
1678 Returns None if no issue for this branch, or specific string keywords.
1679 """
1680 raise NotImplementedError()
1681
1682 def GetCodereviewServer(self):
1683 """Returns server URL without end slash, like "https://codereview.com"."""
1684 raise NotImplementedError()
1685
1686 def FetchDescription(self):
1687 """Fetches and returns description from the codereview server."""
1688 raise NotImplementedError()
1689
tandrii5d48c322016-08-18 16:19:37 -07001690 @classmethod
1691 def IssueConfigKey(cls):
1692 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001693 raise NotImplementedError()
1694
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001695 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001696 def PatchsetConfigKey(cls):
1697 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001698 raise NotImplementedError()
1699
tandrii5d48c322016-08-18 16:19:37 -07001700 @classmethod
1701 def CodereviewServerConfigKey(cls):
1702 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001703 raise NotImplementedError()
1704
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001705 def _PostUnsetIssueProperties(self):
1706 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001707 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001708
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001709 def GetRieveldObjForPresubmit(self):
1710 # This is an unfortunate Rietveld-embeddedness in presubmit.
1711 # For non-Rietveld codereviews, this probably should return a dummy object.
1712 raise NotImplementedError()
1713
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001714 def GetGerritObjForPresubmit(self):
1715 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1716 return None
1717
dsansomee2d6fd92016-09-08 00:10:47 -07001718 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001719 """Update the description on codereview site."""
1720 raise NotImplementedError()
1721
1722 def CloseIssue(self):
1723 """Closes the issue."""
1724 raise NotImplementedError()
1725
1726 def GetApprovingReviewers(self):
1727 """Returns a list of reviewers approving the change.
1728
1729 Note: not necessarily committers.
1730 """
1731 raise NotImplementedError()
1732
1733 def GetMostRecentPatchset(self):
1734 """Returns the most recent patchset number from the codereview site."""
1735 raise NotImplementedError()
1736
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001737 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1738 directory):
1739 """Fetches and applies the issue.
1740
1741 Arguments:
1742 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1743 reject: if True, reject the failed patch instead of switching to 3-way
1744 merge. Rietveld only.
1745 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1746 only.
1747 directory: switch to directory before applying the patch. Rietveld only.
1748 """
1749 raise NotImplementedError()
1750
1751 @staticmethod
1752 def ParseIssueURL(parsed_url):
1753 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1754 failed."""
1755 raise NotImplementedError()
1756
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001757 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001758 """Best effort check that user is authenticated with codereview server.
1759
1760 Arguments:
1761 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001762 refresh: whether to attempt to refresh credentials. Ignored if not
1763 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001764 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001765 raise NotImplementedError()
1766
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001767 def CMDUploadChange(self, options, args, change):
1768 """Uploads a change to codereview."""
1769 raise NotImplementedError()
1770
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001771 def SetCQState(self, new_state):
1772 """Update the CQ state for latest patchset.
1773
1774 Issue must have been already uploaded and known.
1775 """
1776 raise NotImplementedError()
1777
tandriie113dfd2016-10-11 10:20:12 -07001778 def CannotTriggerTryJobReason(self):
1779 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1780 raise NotImplementedError()
1781
tandriide281ae2016-10-12 06:02:30 -07001782 def GetIssueOwner(self):
1783 raise NotImplementedError()
1784
tandrii8c5a3532016-11-04 07:52:02 -07001785 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001786 raise NotImplementedError()
1787
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001788
1789class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1790 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1791 super(_RietveldChangelistImpl, self).__init__(changelist)
1792 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001793 if not rietveld_server:
1794 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001795
1796 self._rietveld_server = rietveld_server
1797 self._auth_config = auth_config
1798 self._props = None
1799 self._rpc_server = None
1800
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001801 def GetCodereviewServer(self):
1802 if not self._rietveld_server:
1803 # If we're on a branch then get the server potentially associated
1804 # with that branch.
1805 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001806 self._rietveld_server = gclient_utils.UpgradeToHttps(
1807 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001808 if not self._rietveld_server:
1809 self._rietveld_server = settings.GetDefaultServerUrl()
1810 return self._rietveld_server
1811
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001812 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001813 """Best effort check that user is authenticated with Rietveld server."""
1814 if self._auth_config.use_oauth2:
1815 authenticator = auth.get_authenticator_for_host(
1816 self.GetCodereviewServer(), self._auth_config)
1817 if not authenticator.has_cached_credentials():
1818 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001819 if refresh:
1820 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001821
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001822 def FetchDescription(self):
1823 issue = self.GetIssue()
1824 assert issue
1825 try:
1826 return self.RpcServer().get_description(issue).strip()
1827 except urllib2.HTTPError as e:
1828 if e.code == 404:
1829 DieWithError(
1830 ('\nWhile fetching the description for issue %d, received a '
1831 '404 (not found)\n'
1832 'error. It is likely that you deleted this '
1833 'issue on the server. If this is the\n'
1834 'case, please run\n\n'
1835 ' git cl issue 0\n\n'
1836 'to clear the association with the deleted issue. Then run '
1837 'this command again.') % issue)
1838 else:
1839 DieWithError(
1840 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1841 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001842 print('Warning: Failed to retrieve CL description due to network '
1843 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001844 return ''
1845
1846 def GetMostRecentPatchset(self):
1847 return self.GetIssueProperties()['patchsets'][-1]
1848
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001849 def GetIssueProperties(self):
1850 if self._props is None:
1851 issue = self.GetIssue()
1852 if not issue:
1853 self._props = {}
1854 else:
1855 self._props = self.RpcServer().get_issue_properties(issue, True)
1856 return self._props
1857
tandriie113dfd2016-10-11 10:20:12 -07001858 def CannotTriggerTryJobReason(self):
1859 props = self.GetIssueProperties()
1860 if not props:
1861 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1862 if props.get('closed'):
1863 return 'CL %s is closed' % self.GetIssue()
1864 if props.get('private'):
1865 return 'CL %s is private' % self.GetIssue()
1866 return None
1867
tandrii8c5a3532016-11-04 07:52:02 -07001868 def GetTryjobProperties(self, patchset=None):
1869 """Returns dictionary of properties to launch tryjob."""
1870 project = (self.GetIssueProperties() or {}).get('project')
1871 return {
1872 'issue': self.GetIssue(),
1873 'patch_project': project,
1874 'patch_storage': 'rietveld',
1875 'patchset': patchset or self.GetPatchset(),
1876 'rietveld': self.GetCodereviewServer(),
1877 }
1878
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001879 def GetApprovingReviewers(self):
1880 return get_approving_reviewers(self.GetIssueProperties())
1881
tandriide281ae2016-10-12 06:02:30 -07001882 def GetIssueOwner(self):
1883 return (self.GetIssueProperties() or {}).get('owner_email')
1884
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001885 def AddComment(self, message):
1886 return self.RpcServer().add_comment(self.GetIssue(), message)
1887
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001888 def GetStatus(self):
1889 """Apply a rough heuristic to give a simple summary of an issue's review
1890 or CQ status, assuming adherence to a common workflow.
1891
1892 Returns None if no issue for this branch, or one of the following keywords:
1893 * 'error' - error from review tool (including deleted issues)
1894 * 'unsent' - not sent for review
1895 * 'waiting' - waiting for review
1896 * 'reply' - waiting for owner to reply to review
1897 * 'lgtm' - LGTM from at least one approved reviewer
1898 * 'commit' - in the commit queue
1899 * 'closed' - closed
1900 """
1901 if not self.GetIssue():
1902 return None
1903
1904 try:
1905 props = self.GetIssueProperties()
1906 except urllib2.HTTPError:
1907 return 'error'
1908
1909 if props.get('closed'):
1910 # Issue is closed.
1911 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001912 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001913 # Issue is in the commit queue.
1914 return 'commit'
1915
1916 try:
1917 reviewers = self.GetApprovingReviewers()
1918 except urllib2.HTTPError:
1919 return 'error'
1920
1921 if reviewers:
1922 # Was LGTM'ed.
1923 return 'lgtm'
1924
1925 messages = props.get('messages') or []
1926
tandrii9d2c7a32016-06-22 03:42:45 -07001927 # Skip CQ messages that don't require owner's action.
1928 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1929 if 'Dry run:' in messages[-1]['text']:
1930 messages.pop()
1931 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1932 # This message always follows prior messages from CQ,
1933 # so skip this too.
1934 messages.pop()
1935 else:
1936 # This is probably a CQ messages warranting user attention.
1937 break
1938
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001939 if not messages:
1940 # No message was sent.
1941 return 'unsent'
1942 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001943 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001944 return 'reply'
1945 return 'waiting'
1946
dsansomee2d6fd92016-09-08 00:10:47 -07001947 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001948 return self.RpcServer().update_description(
1949 self.GetIssue(), self.description)
1950
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001952 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001953
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001954 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001955 return self.SetFlags({flag: value})
1956
1957 def SetFlags(self, flags):
1958 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001959 """
phajdan.jr68598232016-08-10 03:28:28 -07001960 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001961 try:
tandrii4b233bd2016-07-06 03:50:29 -07001962 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001963 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001964 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001965 if e.code == 404:
1966 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1967 if e.code == 403:
1968 DieWithError(
1969 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001970 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001971 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001972
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001973 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974 """Returns an upload.RpcServer() to access this review's rietveld instance.
1975 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001976 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001977 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001978 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001979 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001980 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001981
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001982 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001983 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001984 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001985
tandrii5d48c322016-08-18 16:19:37 -07001986 @classmethod
1987 def PatchsetConfigKey(cls):
1988 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001989
tandrii5d48c322016-08-18 16:19:37 -07001990 @classmethod
1991 def CodereviewServerConfigKey(cls):
1992 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001993
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001994 def GetRieveldObjForPresubmit(self):
1995 return self.RpcServer()
1996
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001997 def SetCQState(self, new_state):
1998 props = self.GetIssueProperties()
1999 if props.get('private'):
2000 DieWithError('Cannot set-commit on private issue')
2001
2002 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002003 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002004 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002005 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002006 else:
tandrii4b233bd2016-07-06 03:50:29 -07002007 assert new_state == _CQState.DRY_RUN
2008 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002009
2010
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002011 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2012 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002013 # PatchIssue should never be called with a dirty tree. It is up to the
2014 # caller to check this, but just in case we assert here since the
2015 # consequences of the caller not checking this could be dire.
2016 assert(not git_common.is_dirty_git_tree('apply'))
2017 assert(parsed_issue_arg.valid)
2018 self._changelist.issue = parsed_issue_arg.issue
2019 if parsed_issue_arg.hostname:
2020 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2021
skobes6468b902016-10-24 08:45:10 -07002022 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2023 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2024 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002025 try:
skobes6468b902016-10-24 08:45:10 -07002026 scm_obj.apply_patch(patchset_object)
2027 except Exception as e:
2028 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002029 return 1
2030
2031 # If we had an issue, commit the current state and register the issue.
2032 if not nocommit:
2033 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2034 'patch from issue %(i)s at patchset '
2035 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2036 % {'i': self.GetIssue(), 'p': patchset})])
2037 self.SetIssue(self.GetIssue())
2038 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002039 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002040 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002041 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002042 return 0
2043
2044 @staticmethod
2045 def ParseIssueURL(parsed_url):
2046 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2047 return None
wychen3c1c1722016-08-04 11:46:36 -07002048 # Rietveld patch: https://domain/<number>/#ps<patchset>
2049 match = re.match(r'/(\d+)/$', parsed_url.path)
2050 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2051 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002052 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002053 issue=int(match.group(1)),
2054 patchset=int(match2.group(1)),
2055 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002056 # Typical url: https://domain/<issue_number>[/[other]]
2057 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2058 if match:
skobes6468b902016-10-24 08:45:10 -07002059 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002060 issue=int(match.group(1)),
2061 hostname=parsed_url.netloc)
2062 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2063 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2064 if match:
skobes6468b902016-10-24 08:45:10 -07002065 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002066 issue=int(match.group(1)),
2067 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002068 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002069 return None
2070
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002071 def CMDUploadChange(self, options, args, change):
2072 """Upload the patch to Rietveld."""
2073 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2074 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002075 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2076 if options.emulate_svn_auto_props:
2077 upload_args.append('--emulate_svn_auto_props')
2078
2079 change_desc = None
2080
2081 if options.email is not None:
2082 upload_args.extend(['--email', options.email])
2083
2084 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002085 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002086 upload_args.extend(['--title', options.title])
2087 if options.message:
2088 upload_args.extend(['--message', options.message])
2089 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002090 print('This branch is associated with issue %s. '
2091 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002092 else:
nodirca166002016-06-27 10:59:51 -07002093 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002094 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002095 if options.message:
2096 message = options.message
2097 else:
2098 message = CreateDescriptionFromLog(args)
2099 if options.title:
2100 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002101 change_desc = ChangeDescription(message)
2102 if options.reviewers or options.tbr_owners:
2103 change_desc.update_reviewers(options.reviewers,
2104 options.tbr_owners,
2105 change)
2106 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002107 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002108
2109 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002110 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002111 return 1
2112
2113 upload_args.extend(['--message', change_desc.description])
2114 if change_desc.get_reviewers():
2115 upload_args.append('--reviewers=%s' % ','.join(
2116 change_desc.get_reviewers()))
2117 if options.send_mail:
2118 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002119 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002120 upload_args.append('--send_mail')
2121
2122 # We check this before applying rietveld.private assuming that in
2123 # rietveld.cc only addresses which we can send private CLs to are listed
2124 # if rietveld.private is set, and so we should ignore rietveld.cc only
2125 # when --private is specified explicitly on the command line.
2126 if options.private:
2127 logging.warn('rietveld.cc is ignored since private flag is specified. '
2128 'You need to review and add them manually if necessary.')
2129 cc = self.GetCCListWithoutDefault()
2130 else:
2131 cc = self.GetCCList()
2132 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002133 if change_desc.get_cced():
2134 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002135 if cc:
2136 upload_args.extend(['--cc', cc])
2137
2138 if options.private or settings.GetDefaultPrivateFlag() == "True":
2139 upload_args.append('--private')
2140
2141 upload_args.extend(['--git_similarity', str(options.similarity)])
2142 if not options.find_copies:
2143 upload_args.extend(['--git_no_find_copies'])
2144
2145 # Include the upstream repo's URL in the change -- this is useful for
2146 # projects that have their source spread across multiple repos.
2147 remote_url = self.GetGitBaseUrlFromConfig()
2148 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002149 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2150 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2151 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002152 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002153 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002154 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002155 if target_ref:
2156 upload_args.extend(['--target_ref', target_ref])
2157
2158 # Look for dependent patchsets. See crbug.com/480453 for more details.
2159 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2160 upstream_branch = ShortBranchName(upstream_branch)
2161 if remote is '.':
2162 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002163 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002164 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002165 print()
2166 print('Skipping dependency patchset upload because git config '
2167 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2168 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002169 else:
2170 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002171 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002172 auth_config=auth_config)
2173 branch_cl_issue_url = branch_cl.GetIssueURL()
2174 branch_cl_issue = branch_cl.GetIssue()
2175 branch_cl_patchset = branch_cl.GetPatchset()
2176 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2177 upload_args.extend(
2178 ['--depends_on_patchset', '%s:%s' % (
2179 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002180 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002181 '\n'
2182 'The current branch (%s) is tracking a local branch (%s) with '
2183 'an associated CL.\n'
2184 'Adding %s/#ps%s as a dependency patchset.\n'
2185 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2186 branch_cl_patchset))
2187
2188 project = settings.GetProject()
2189 if project:
2190 upload_args.extend(['--project', project])
2191
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002192 try:
2193 upload_args = ['upload'] + upload_args + args
2194 logging.info('upload.RealMain(%s)', upload_args)
2195 issue, patchset = upload.RealMain(upload_args)
2196 issue = int(issue)
2197 patchset = int(patchset)
2198 except KeyboardInterrupt:
2199 sys.exit(1)
2200 except:
2201 # If we got an exception after the user typed a description for their
2202 # change, back up the description before re-raising.
2203 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002204 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002205 raise
2206
2207 if not self.GetIssue():
2208 self.SetIssue(issue)
2209 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002210 return 0
2211
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002212class _GerritChangelistImpl(_ChangelistCodereviewBase):
2213 def __init__(self, changelist, auth_config=None):
2214 # auth_config is Rietveld thing, kept here to preserve interface only.
2215 super(_GerritChangelistImpl, self).__init__(changelist)
2216 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002217 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002218 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002219 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002220 # Map from change number (issue) to its detail cache.
2221 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002222
2223 def _GetGerritHost(self):
2224 # Lazy load of configs.
2225 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002226 if self._gerrit_host and '.' not in self._gerrit_host:
2227 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2228 # This happens for internal stuff http://crbug.com/614312.
2229 parsed = urlparse.urlparse(self.GetRemoteUrl())
2230 if parsed.scheme == 'sso':
2231 print('WARNING: using non https URLs for remote is likely broken\n'
2232 ' Your current remote is: %s' % self.GetRemoteUrl())
2233 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2234 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002235 return self._gerrit_host
2236
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002237 def _GetGitHost(self):
2238 """Returns git host to be used when uploading change to Gerrit."""
2239 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2240
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002241 def GetCodereviewServer(self):
2242 if not self._gerrit_server:
2243 # If we're on a branch then get the server potentially associated
2244 # with that branch.
2245 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002246 self._gerrit_server = self._GitGetBranchConfigValue(
2247 self.CodereviewServerConfigKey())
2248 if self._gerrit_server:
2249 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002250 if not self._gerrit_server:
2251 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2252 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002253 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002254 parts[0] = parts[0] + '-review'
2255 self._gerrit_host = '.'.join(parts)
2256 self._gerrit_server = 'https://%s' % self._gerrit_host
2257 return self._gerrit_server
2258
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002259 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002260 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002261 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002262
tandrii5d48c322016-08-18 16:19:37 -07002263 @classmethod
2264 def PatchsetConfigKey(cls):
2265 return 'gerritpatchset'
2266
2267 @classmethod
2268 def CodereviewServerConfigKey(cls):
2269 return 'gerritserver'
2270
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002271 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002272 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002273 if settings.GetGerritSkipEnsureAuthenticated():
2274 # For projects with unusual authentication schemes.
2275 # See http://crbug.com/603378.
2276 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002277 # Lazy-loader to identify Gerrit and Git hosts.
2278 if gerrit_util.GceAuthenticator.is_gce():
2279 return
2280 self.GetCodereviewServer()
2281 git_host = self._GetGitHost()
2282 assert self._gerrit_server and self._gerrit_host
2283 cookie_auth = gerrit_util.CookiesAuthenticator()
2284
2285 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2286 git_auth = cookie_auth.get_auth_header(git_host)
2287 if gerrit_auth and git_auth:
2288 if gerrit_auth == git_auth:
2289 return
2290 print((
2291 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2292 ' Check your %s or %s file for credentials of hosts:\n'
2293 ' %s\n'
2294 ' %s\n'
2295 ' %s') %
2296 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2297 git_host, self._gerrit_host,
2298 cookie_auth.get_new_password_message(git_host)))
2299 if not force:
2300 ask_for_data('If you know what you are doing, press Enter to continue, '
2301 'Ctrl+C to abort.')
2302 return
2303 else:
2304 missing = (
2305 [] if gerrit_auth else [self._gerrit_host] +
2306 [] if git_auth else [git_host])
2307 DieWithError('Credentials for the following hosts are required:\n'
2308 ' %s\n'
2309 'These are read from %s (or legacy %s)\n'
2310 '%s' % (
2311 '\n '.join(missing),
2312 cookie_auth.get_gitcookies_path(),
2313 cookie_auth.get_netrc_path(),
2314 cookie_auth.get_new_password_message(git_host)))
2315
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002316 def _PostUnsetIssueProperties(self):
2317 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002318 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002319
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002320 def GetRieveldObjForPresubmit(self):
2321 class ThisIsNotRietveldIssue(object):
2322 def __nonzero__(self):
2323 # This is a hack to make presubmit_support think that rietveld is not
2324 # defined, yet still ensure that calls directly result in a decent
2325 # exception message below.
2326 return False
2327
2328 def __getattr__(self, attr):
2329 print(
2330 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2331 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2332 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2333 'or use Rietveld for codereview.\n'
2334 'See also http://crbug.com/579160.' % attr)
2335 raise NotImplementedError()
2336 return ThisIsNotRietveldIssue()
2337
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002338 def GetGerritObjForPresubmit(self):
2339 return presubmit_support.GerritAccessor(self._GetGerritHost())
2340
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002341 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002342 """Apply a rough heuristic to give a simple summary of an issue's review
2343 or CQ status, assuming adherence to a common workflow.
2344
2345 Returns None if no issue for this branch, or one of the following keywords:
2346 * 'error' - error from review tool (including deleted issues)
2347 * 'unsent' - no reviewers added
2348 * 'waiting' - waiting for review
2349 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002350 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002351 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002352 * 'commit' - in the commit queue
2353 * 'closed' - abandoned
2354 """
2355 if not self.GetIssue():
2356 return None
2357
2358 try:
2359 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002360 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002361 return 'error'
2362
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002363 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002364 return 'closed'
2365
2366 cq_label = data['labels'].get('Commit-Queue', {})
2367 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002368 votes = cq_label.get('all', [])
2369 highest_vote = 0
2370 for v in votes:
2371 highest_vote = max(highest_vote, v.get('value', 0))
2372 vote_value = str(highest_vote)
2373 if vote_value != '0':
2374 # Add a '+' if the value is not 0 to match the values in the label.
2375 # The cq_label does not have negatives.
2376 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002377 vote_text = cq_label.get('values', {}).get(vote_value, '')
2378 if vote_text.lower() == 'commit':
2379 return 'commit'
2380
2381 lgtm_label = data['labels'].get('Code-Review', {})
2382 if lgtm_label:
2383 if 'rejected' in lgtm_label:
2384 return 'not lgtm'
2385 if 'approved' in lgtm_label:
2386 return 'lgtm'
2387
2388 if not data.get('reviewers', {}).get('REVIEWER', []):
2389 return 'unsent'
2390
2391 messages = data.get('messages', [])
2392 if messages:
2393 owner = data['owner'].get('_account_id')
2394 last_message_author = messages[-1].get('author', {}).get('_account_id')
2395 if owner != last_message_author:
2396 # Some reply from non-owner.
2397 return 'reply'
2398
2399 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002400
2401 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002402 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002403 return data['revisions'][data['current_revision']]['_number']
2404
2405 def FetchDescription(self):
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002406 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002407 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002408 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002409
dsansomee2d6fd92016-09-08 00:10:47 -07002410 def UpdateDescriptionRemote(self, description, force=False):
2411 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2412 if not force:
2413 ask_for_data(
2414 'The description cannot be modified while the issue has a pending '
2415 'unpublished edit. Either publish the edit in the Gerrit web UI '
2416 'or delete it.\n\n'
2417 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2418
2419 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2420 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002421 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002422 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002423
2424 def CloseIssue(self):
2425 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2426
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002427 def GetApprovingReviewers(self):
2428 """Returns a list of reviewers approving the change.
2429
2430 Note: not necessarily committers.
2431 """
2432 raise NotImplementedError()
2433
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002434 def SubmitIssue(self, wait_for_merge=True):
2435 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2436 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002437
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002438 def _GetChangeDetail(self, options=None, issue=None,
2439 no_cache=False):
2440 """Returns details of the issue by querying Gerrit and caching results.
2441
2442 If fresh data is needed, set no_cache=True which will clear cache and
2443 thus new data will be fetched from Gerrit.
2444 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002445 options = options or []
2446 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002447 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002448
2449 # Normalize issue and options for consistent keys in cache.
2450 issue = str(issue)
2451 options = [o.upper() for o in options]
2452
2453 # Check in cache first unless no_cache is True.
2454 if no_cache:
2455 self._detail_cache.pop(issue, None)
2456 else:
2457 options_set = frozenset(options)
2458 for cached_options_set, data in self._detail_cache.get(issue, []):
2459 # Assumption: data fetched before with extra options is suitable
2460 # for return for a smaller set of options.
2461 # For example, if we cached data for
2462 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2463 # and request is for options=[CURRENT_REVISION],
2464 # THEN we can return prior cached data.
2465 if options_set.issubset(cached_options_set):
2466 return data
2467
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002468 try:
2469 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2470 options, ignore_404=False)
2471 except gerrit_util.GerritError as e:
2472 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002473 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002474 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002475
2476 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002477 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002478
agable32978d92016-11-01 12:55:02 -07002479 def _GetChangeCommit(self, issue=None):
2480 issue = issue or self.GetIssue()
2481 assert issue, 'issue is required to query Gerrit'
2482 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2483 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002484 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002485 return data
2486
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002487 def CMDLand(self, force, bypass_hooks, verbose):
2488 if git_common.is_dirty_git_tree('land'):
2489 return 1
tandriid60367b2016-06-22 05:25:12 -07002490 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2491 if u'Commit-Queue' in detail.get('labels', {}):
2492 if not force:
2493 ask_for_data('\nIt seems this repository has a Commit Queue, '
2494 'which can test and land changes for you. '
2495 'Are you sure you wish to bypass it?\n'
2496 'Press Enter to continue, Ctrl+C to abort.')
2497
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002498 differs = True
tandriic4344b52016-08-29 06:04:54 -07002499 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002500 # Note: git diff outputs nothing if there is no diff.
2501 if not last_upload or RunGit(['diff', last_upload]).strip():
2502 print('WARNING: some changes from local branch haven\'t been uploaded')
2503 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002504 if detail['current_revision'] == last_upload:
2505 differs = False
2506 else:
2507 print('WARNING: local branch contents differ from latest uploaded '
2508 'patchset')
2509 if differs:
2510 if not force:
2511 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002512 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2513 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002514 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2515 elif not bypass_hooks:
2516 hook_results = self.RunHook(
2517 committing=True,
2518 may_prompt=not force,
2519 verbose=verbose,
2520 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2521 if not hook_results.should_continue():
2522 return 1
2523
2524 self.SubmitIssue(wait_for_merge=True)
2525 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002526 links = self._GetChangeCommit().get('web_links', [])
2527 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002528 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002529 print('Landed as %s' % link.get('url'))
2530 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002531 return 0
2532
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002533 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2534 directory):
2535 assert not reject
2536 assert not nocommit
2537 assert not directory
2538 assert parsed_issue_arg.valid
2539
2540 self._changelist.issue = parsed_issue_arg.issue
2541
2542 if parsed_issue_arg.hostname:
2543 self._gerrit_host = parsed_issue_arg.hostname
2544 self._gerrit_server = 'https://%s' % self._gerrit_host
2545
tandriic2405f52016-10-10 08:13:15 -07002546 try:
2547 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002548 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002549 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002550
2551 if not parsed_issue_arg.patchset:
2552 # Use current revision by default.
2553 revision_info = detail['revisions'][detail['current_revision']]
2554 patchset = int(revision_info['_number'])
2555 else:
2556 patchset = parsed_issue_arg.patchset
2557 for revision_info in detail['revisions'].itervalues():
2558 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2559 break
2560 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002561 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002562 (parsed_issue_arg.patchset, self.GetIssue()))
2563
2564 fetch_info = revision_info['fetch']['http']
2565 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2566 RunGit(['cherry-pick', 'FETCH_HEAD'])
2567 self.SetIssue(self.GetIssue())
2568 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002569 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002570 (self.GetIssue(), self.GetPatchset()))
2571 return 0
2572
2573 @staticmethod
2574 def ParseIssueURL(parsed_url):
2575 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2576 return None
2577 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2578 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2579 # Short urls like https://domain/<issue_number> can be used, but don't allow
2580 # specifying the patchset (you'd 404), but we allow that here.
2581 if parsed_url.path == '/':
2582 part = parsed_url.fragment
2583 else:
2584 part = parsed_url.path
2585 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2586 if match:
2587 return _ParsedIssueNumberArgument(
2588 issue=int(match.group(2)),
2589 patchset=int(match.group(4)) if match.group(4) else None,
2590 hostname=parsed_url.netloc)
2591 return None
2592
tandrii16e0b4e2016-06-07 10:34:28 -07002593 def _GerritCommitMsgHookCheck(self, offer_removal):
2594 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2595 if not os.path.exists(hook):
2596 return
2597 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2598 # custom developer made one.
2599 data = gclient_utils.FileRead(hook)
2600 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2601 return
2602 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002603 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002604 'and may interfere with it in subtle ways.\n'
2605 'We recommend you remove the commit-msg hook.')
2606 if offer_removal:
2607 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2608 if reply.lower().startswith('y'):
2609 gclient_utils.rm_file_or_tree(hook)
2610 print('Gerrit commit-msg hook removed.')
2611 else:
2612 print('OK, will keep Gerrit commit-msg hook in place.')
2613
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002614 def CMDUploadChange(self, options, args, change):
2615 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002616 if options.squash and options.no_squash:
2617 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002618
2619 if not options.squash and not options.no_squash:
2620 # Load default for user, repo, squash=true, in this order.
2621 options.squash = settings.GetSquashGerritUploads()
2622 elif options.no_squash:
2623 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002624
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002625 # We assume the remote called "origin" is the one we want.
2626 # It is probably not worthwhile to support different workflows.
2627 gerrit_remote = 'origin'
2628
2629 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002630 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002631
Aaron Gableb56ad332017-01-06 15:24:31 -08002632 # This may be None; default fallback value is determined in logic below.
2633 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002634 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002635
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002636 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002637 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002638 if self.GetIssue():
2639 # Try to get the message from a previous upload.
2640 message = self.GetDescription()
2641 if not message:
2642 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002643 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002644 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002645 if not title:
2646 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2647 title = ask_for_data(
2648 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002649 if title == default_title:
2650 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002651 change_id = self._GetChangeDetail()['change_id']
2652 while True:
2653 footer_change_ids = git_footers.get_footer_change_id(message)
2654 if footer_change_ids == [change_id]:
2655 break
2656 if not footer_change_ids:
2657 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002658 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002659 continue
2660 # There is already a valid footer but with different or several ids.
2661 # Doing this automatically is non-trivial as we don't want to lose
2662 # existing other footers, yet we want to append just 1 desired
2663 # Change-Id. Thus, just create a new footer, but let user verify the
2664 # new description.
2665 message = '%s\n\nChange-Id: %s' % (message, change_id)
2666 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002667 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002668 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002669 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002670 'Please, check the proposed correction to the description, '
2671 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2672 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2673 change_id))
2674 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2675 if not options.force:
2676 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002677 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002678 message = change_desc.description
2679 if not message:
2680 DieWithError("Description is empty. Aborting...")
2681 # Continue the while loop.
2682 # Sanity check of this code - we should end up with proper message
2683 # footer.
2684 assert [change_id] == git_footers.get_footer_change_id(message)
2685 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002686 else: # if not self.GetIssue()
2687 if options.message:
2688 message = options.message
2689 else:
2690 message = CreateDescriptionFromLog(args)
2691 if options.title:
2692 message = options.title + '\n\n' + message
2693 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002694 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002695 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002696 # On first upload, patchset title is always this string, while
2697 # --title flag gets converted to first line of message.
2698 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002699 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002700 if not change_desc.description:
2701 DieWithError("Description is empty. Aborting...")
2702 message = change_desc.description
2703 change_ids = git_footers.get_footer_change_id(message)
2704 if len(change_ids) > 1:
2705 DieWithError('too many Change-Id footers, at most 1 allowed.')
2706 if not change_ids:
2707 # Generate the Change-Id automatically.
2708 message = git_footers.add_footer_change_id(
2709 message, GenerateGerritChangeId(message))
2710 change_desc.set_description(message)
2711 change_ids = git_footers.get_footer_change_id(message)
2712 assert len(change_ids) == 1
2713 change_id = change_ids[0]
2714
2715 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2716 if remote is '.':
2717 # If our upstream branch is local, we base our squashed commit on its
2718 # squashed version.
2719 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2720 # Check the squashed hash of the parent.
2721 parent = RunGit(['config',
2722 'branch.%s.gerritsquashhash' % upstream_branch_name],
2723 error_ok=True).strip()
2724 # Verify that the upstream branch has been uploaded too, otherwise
2725 # Gerrit will create additional CLs when uploading.
2726 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2727 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002728 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002729 '\nUpload upstream branch %s first.\n'
2730 'It is likely that this branch has been rebased since its last '
2731 'upload, so you just need to upload it again.\n'
2732 '(If you uploaded it with --no-squash, then branch dependencies '
2733 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002734 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002735 else:
2736 parent = self.GetCommonAncestorWithUpstream()
2737
2738 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2739 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2740 '-m', message]).strip()
2741 else:
2742 change_desc = ChangeDescription(
2743 options.message or CreateDescriptionFromLog(args))
2744 if not change_desc.description:
2745 DieWithError("Description is empty. Aborting...")
2746
2747 if not git_footers.get_footer_change_id(change_desc.description):
2748 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002749 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2750 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002751 ref_to_push = 'HEAD'
2752 parent = '%s/%s' % (gerrit_remote, branch)
2753 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2754
2755 assert change_desc
2756 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2757 ref_to_push)]).splitlines()
2758 if len(commits) > 1:
2759 print('WARNING: This will upload %d commits. Run the following command '
2760 'to see which commits will be uploaded: ' % len(commits))
2761 print('git log %s..%s' % (parent, ref_to_push))
2762 print('You can also use `git squash-branch` to squash these into a '
2763 'single commit.')
2764 ask_for_data('About to upload; enter to confirm.')
2765
2766 if options.reviewers or options.tbr_owners:
2767 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2768 change)
2769
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002770 # Extra options that can be specified at push time. Doc:
2771 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2772 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002773 if change_desc.get_reviewers(tbr_only=True):
2774 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2775 refspec_opts.append('l=Code-Review+1')
2776
Aaron Gable9b713dd2016-12-14 16:04:21 -08002777 if title:
2778 if not re.match(r'^[\w ]+$', title):
2779 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002780 if not automatic_title:
2781 print('WARNING: Patchset title may only contain alphanumeric chars '
2782 'and spaces. Cleaned up title:\n%s' % title)
2783 if not options.force:
2784 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002785 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2786 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002787 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002788
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002789 if options.send_mail:
2790 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002791 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002792 refspec_opts.append('notify=ALL')
2793 else:
2794 refspec_opts.append('notify=NONE')
2795
tandrii99a72f22016-08-17 14:33:24 -07002796 reviewers = change_desc.get_reviewers()
2797 if reviewers:
2798 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002799
agablec6787972016-09-09 16:13:34 -07002800 if options.private:
2801 refspec_opts.append('draft')
2802
rmistry9eadede2016-09-19 11:22:43 -07002803 if options.topic:
2804 # Documentation on Gerrit topics is here:
2805 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2806 refspec_opts.append('topic=%s' % options.topic)
2807
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002808 refspec_suffix = ''
2809 if refspec_opts:
2810 refspec_suffix = '%' + ','.join(refspec_opts)
2811 assert ' ' not in refspec_suffix, (
2812 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002813 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002814
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002815 try:
2816 push_stdout = gclient_utils.CheckCallAndFilter(
2817 ['git', 'push', gerrit_remote, refspec],
2818 print_stdout=True,
2819 # Flush after every line: useful for seeing progress when running as
2820 # recipe.
2821 filter_fn=lambda _: sys.stdout.flush())
2822 except subprocess2.CalledProcessError:
2823 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002824 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002825
2826 if options.squash:
2827 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2828 change_numbers = [m.group(1)
2829 for m in map(regex.match, push_stdout.splitlines())
2830 if m]
2831 if len(change_numbers) != 1:
2832 DieWithError(
2833 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002834 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002835 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002836 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002837
2838 # Add cc's from the CC_LIST and --cc flag (if any).
2839 cc = self.GetCCList().split(',')
2840 if options.cc:
2841 cc.extend(options.cc)
2842 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002843 if change_desc.get_cced():
2844 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002845 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002846 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002847 self._GetGerritHost(), self.GetIssue(), cc,
2848 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002849 return 0
2850
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002851 def _AddChangeIdToCommitMessage(self, options, args):
2852 """Re-commits using the current message, assumes the commit hook is in
2853 place.
2854 """
2855 log_desc = options.message or CreateDescriptionFromLog(args)
2856 git_command = ['commit', '--amend', '-m', log_desc]
2857 RunGit(git_command)
2858 new_log_desc = CreateDescriptionFromLog(args)
2859 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002860 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002861 return new_log_desc
2862 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002863 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002864
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002865 def SetCQState(self, new_state):
2866 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002867 vote_map = {
2868 _CQState.NONE: 0,
2869 _CQState.DRY_RUN: 1,
2870 _CQState.COMMIT : 2,
2871 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002872 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2873 if new_state == _CQState.DRY_RUN:
2874 # Don't spam everybody reviewer/owner.
2875 kwargs['notify'] = 'NONE'
2876 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002877
tandriie113dfd2016-10-11 10:20:12 -07002878 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002879 try:
2880 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002881 except GerritChangeNotExists:
2882 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002883
2884 if data['status'] in ('ABANDONED', 'MERGED'):
2885 return 'CL %s is closed' % self.GetIssue()
2886
2887 def GetTryjobProperties(self, patchset=None):
2888 """Returns dictionary of properties to launch tryjob."""
2889 data = self._GetChangeDetail(['ALL_REVISIONS'])
2890 patchset = int(patchset or self.GetPatchset())
2891 assert patchset
2892 revision_data = None # Pylint wants it to be defined.
2893 for revision_data in data['revisions'].itervalues():
2894 if int(revision_data['_number']) == patchset:
2895 break
2896 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002897 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002898 (patchset, self.GetIssue()))
2899 return {
2900 'patch_issue': self.GetIssue(),
2901 'patch_set': patchset or self.GetPatchset(),
2902 'patch_project': data['project'],
2903 'patch_storage': 'gerrit',
2904 'patch_ref': revision_data['fetch']['http']['ref'],
2905 'patch_repository_url': revision_data['fetch']['http']['url'],
2906 'patch_gerrit_url': self.GetCodereviewServer(),
2907 }
tandriie113dfd2016-10-11 10:20:12 -07002908
tandriide281ae2016-10-12 06:02:30 -07002909 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002910 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002911
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002912
2913_CODEREVIEW_IMPLEMENTATIONS = {
2914 'rietveld': _RietveldChangelistImpl,
2915 'gerrit': _GerritChangelistImpl,
2916}
2917
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002918
iannuccie53c9352016-08-17 14:40:40 -07002919def _add_codereview_issue_select_options(parser, extra=""):
2920 _add_codereview_select_options(parser)
2921
2922 text = ('Operate on this issue number instead of the current branch\'s '
2923 'implicit issue.')
2924 if extra:
2925 text += ' '+extra
2926 parser.add_option('-i', '--issue', type=int, help=text)
2927
2928
2929def _process_codereview_issue_select_options(parser, options):
2930 _process_codereview_select_options(parser, options)
2931 if options.issue is not None and not options.forced_codereview:
2932 parser.error('--issue must be specified with either --rietveld or --gerrit')
2933
2934
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002935def _add_codereview_select_options(parser):
2936 """Appends --gerrit and --rietveld options to force specific codereview."""
2937 parser.codereview_group = optparse.OptionGroup(
2938 parser, 'EXPERIMENTAL! Codereview override options')
2939 parser.add_option_group(parser.codereview_group)
2940 parser.codereview_group.add_option(
2941 '--gerrit', action='store_true',
2942 help='Force the use of Gerrit for codereview')
2943 parser.codereview_group.add_option(
2944 '--rietveld', action='store_true',
2945 help='Force the use of Rietveld for codereview')
2946
2947
2948def _process_codereview_select_options(parser, options):
2949 if options.gerrit and options.rietveld:
2950 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2951 options.forced_codereview = None
2952 if options.gerrit:
2953 options.forced_codereview = 'gerrit'
2954 elif options.rietveld:
2955 options.forced_codereview = 'rietveld'
2956
2957
tandriif9aefb72016-07-01 09:06:51 -07002958def _get_bug_line_values(default_project, bugs):
2959 """Given default_project and comma separated list of bugs, yields bug line
2960 values.
2961
2962 Each bug can be either:
2963 * a number, which is combined with default_project
2964 * string, which is left as is.
2965
2966 This function may produce more than one line, because bugdroid expects one
2967 project per line.
2968
2969 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2970 ['v8:123', 'chromium:789']
2971 """
2972 default_bugs = []
2973 others = []
2974 for bug in bugs.split(','):
2975 bug = bug.strip()
2976 if bug:
2977 try:
2978 default_bugs.append(int(bug))
2979 except ValueError:
2980 others.append(bug)
2981
2982 if default_bugs:
2983 default_bugs = ','.join(map(str, default_bugs))
2984 if default_project:
2985 yield '%s:%s' % (default_project, default_bugs)
2986 else:
2987 yield default_bugs
2988 for other in sorted(others):
2989 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2990 yield other
2991
2992
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002993class ChangeDescription(object):
2994 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002995 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002996 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002997 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002998 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002999
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003000 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003001 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003002
agable@chromium.org42c20792013-09-12 17:34:49 +00003003 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003004 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003005 return '\n'.join(self._description_lines)
3006
3007 def set_description(self, desc):
3008 if isinstance(desc, basestring):
3009 lines = desc.splitlines()
3010 else:
3011 lines = [line.rstrip() for line in desc]
3012 while lines and not lines[0]:
3013 lines.pop(0)
3014 while lines and not lines[-1]:
3015 lines.pop(-1)
3016 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003017
piman@chromium.org336f9122014-09-04 02:16:55 +00003018 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003019 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003020 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003021 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003022 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003023 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003024
agable@chromium.org42c20792013-09-12 17:34:49 +00003025 # Get the set of R= and TBR= lines and remove them from the desciption.
3026 regexp = re.compile(self.R_LINE)
3027 matches = [regexp.match(line) for line in self._description_lines]
3028 new_desc = [l for i, l in enumerate(self._description_lines)
3029 if not matches[i]]
3030 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003031
agable@chromium.org42c20792013-09-12 17:34:49 +00003032 # Construct new unified R= and TBR= lines.
3033 r_names = []
3034 tbr_names = []
3035 for match in matches:
3036 if not match:
3037 continue
3038 people = cleanup_list([match.group(2).strip()])
3039 if match.group(1) == 'TBR':
3040 tbr_names.extend(people)
3041 else:
3042 r_names.extend(people)
3043 for name in r_names:
3044 if name not in reviewers:
3045 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003046 if add_owners_tbr:
3047 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003048 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003049 all_reviewers = set(tbr_names + reviewers)
3050 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3051 all_reviewers)
3052 tbr_names.extend(owners_db.reviewers_for(missing_files,
3053 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003054 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3055 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3056
3057 # Put the new lines in the description where the old first R= line was.
3058 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3059 if 0 <= line_loc < len(self._description_lines):
3060 if new_tbr_line:
3061 self._description_lines.insert(line_loc, new_tbr_line)
3062 if new_r_line:
3063 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003064 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003065 if new_r_line:
3066 self.append_footer(new_r_line)
3067 if new_tbr_line:
3068 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003069
tandriif9aefb72016-07-01 09:06:51 -07003070 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003071 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003072 self.set_description([
3073 '# Enter a description of the change.',
3074 '# This will be displayed on the codereview site.',
3075 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003076 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003077 '--------------------',
3078 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003079
agable@chromium.org42c20792013-09-12 17:34:49 +00003080 regexp = re.compile(self.BUG_LINE)
3081 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003082 prefix = settings.GetBugPrefix()
3083 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3084 for value in values:
3085 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3086 self.append_footer('BUG=%s' % value)
3087
agable@chromium.org42c20792013-09-12 17:34:49 +00003088 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003089 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003090 if not content:
3091 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003092 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003093
3094 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003095 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3096 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003097 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003098 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003099
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003100 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003101 """Adds a footer line to the description.
3102
3103 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3104 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3105 that Gerrit footers are always at the end.
3106 """
3107 parsed_footer_line = git_footers.parse_footer(line)
3108 if parsed_footer_line:
3109 # Line is a gerrit footer in the form: Footer-Key: any value.
3110 # Thus, must be appended observing Gerrit footer rules.
3111 self.set_description(
3112 git_footers.add_footer(self.description,
3113 key=parsed_footer_line[0],
3114 value=parsed_footer_line[1]))
3115 return
3116
3117 if not self._description_lines:
3118 self._description_lines.append(line)
3119 return
3120
3121 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3122 if gerrit_footers:
3123 # git_footers.split_footers ensures that there is an empty line before
3124 # actual (gerrit) footers, if any. We have to keep it that way.
3125 assert top_lines and top_lines[-1] == ''
3126 top_lines, separator = top_lines[:-1], top_lines[-1:]
3127 else:
3128 separator = [] # No need for separator if there are no gerrit_footers.
3129
3130 prev_line = top_lines[-1] if top_lines else ''
3131 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3132 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3133 top_lines.append('')
3134 top_lines.append(line)
3135 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003136
tandrii99a72f22016-08-17 14:33:24 -07003137 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003138 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003139 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003140 reviewers = [match.group(2).strip()
3141 for match in matches
3142 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003143 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003144
bradnelsond975b302016-10-23 12:20:23 -07003145 def get_cced(self):
3146 """Retrieves the list of reviewers."""
3147 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3148 cced = [match.group(2).strip() for match in matches if match]
3149 return cleanup_list(cced)
3150
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003151 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3152 """Updates this commit description given the parent.
3153
3154 This is essentially what Gnumbd used to do.
3155 Consult https://goo.gl/WMmpDe for more details.
3156 """
3157 assert parent_msg # No, orphan branch creation isn't supported.
3158 assert parent_hash
3159 assert dest_ref
3160 parent_footer_map = git_footers.parse_footers(parent_msg)
3161 # This will also happily parse svn-position, which GnumbD is no longer
3162 # supporting. While we'd generate correct footers, the verifier plugin
3163 # installed in Gerrit will block such commit (ie git push below will fail).
3164 parent_position = git_footers.get_position(parent_footer_map)
3165
3166 # Cherry-picks may have last line obscuring their prior footers,
3167 # from git_footers perspective. This is also what Gnumbd did.
3168 cp_line = None
3169 if (self._description_lines and
3170 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3171 cp_line = self._description_lines.pop()
3172
3173 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3174
3175 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3176 # user interference with actual footers we'd insert below.
3177 for i, (k, v) in enumerate(parsed_footers):
3178 if k.startswith('Cr-'):
3179 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3180
3181 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003182 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003183 if parent_position[0] == dest_ref:
3184 # Same branch as parent.
3185 number = int(parent_position[1]) + 1
3186 else:
3187 number = 1 # New branch, and extra lineage.
3188 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3189 int(parent_position[1])))
3190
3191 parsed_footers.append(('Cr-Commit-Position',
3192 '%s@{#%d}' % (dest_ref, number)))
3193 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3194
3195 self._description_lines = top_lines
3196 if cp_line:
3197 self._description_lines.append(cp_line)
3198 if self._description_lines[-1] != '':
3199 self._description_lines.append('') # Ensure footer separator.
3200 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3201
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003202
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003203def get_approving_reviewers(props):
3204 """Retrieves the reviewers that approved a CL from the issue properties with
3205 messages.
3206
3207 Note that the list may contain reviewers that are not committer, thus are not
3208 considered by the CQ.
3209 """
3210 return sorted(
3211 set(
3212 message['sender']
3213 for message in props['messages']
3214 if message['approval'] and message['sender'] in props['reviewers']
3215 )
3216 )
3217
3218
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003219def FindCodereviewSettingsFile(filename='codereview.settings'):
3220 """Finds the given file starting in the cwd and going up.
3221
3222 Only looks up to the top of the repository unless an
3223 'inherit-review-settings-ok' file exists in the root of the repository.
3224 """
3225 inherit_ok_file = 'inherit-review-settings-ok'
3226 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003227 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003228 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3229 root = '/'
3230 while True:
3231 if filename in os.listdir(cwd):
3232 if os.path.isfile(os.path.join(cwd, filename)):
3233 return open(os.path.join(cwd, filename))
3234 if cwd == root:
3235 break
3236 cwd = os.path.dirname(cwd)
3237
3238
3239def LoadCodereviewSettingsFromFile(fileobj):
3240 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003241 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003243 def SetProperty(name, setting, unset_error_ok=False):
3244 fullname = 'rietveld.' + name
3245 if setting in keyvals:
3246 RunGit(['config', fullname, keyvals[setting]])
3247 else:
3248 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3249
tandrii48df5812016-10-17 03:55:37 -07003250 if not keyvals.get('GERRIT_HOST', False):
3251 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003252 # Only server setting is required. Other settings can be absent.
3253 # In that case, we ignore errors raised during option deletion attempt.
3254 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003255 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003256 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3257 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003258 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003259 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3260 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003261 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003262 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3263 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003264
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003265 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003266 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003267
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003268 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003269 RunGit(['config', 'gerrit.squash-uploads',
3270 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003271
tandrii@chromium.org28253532016-04-14 13:46:56 +00003272 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003273 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003274 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3275
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003276 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3277 #should be of the form
3278 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3279 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3280 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3281 keyvals['ORIGIN_URL_CONFIG']])
3282
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003283
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003284def urlretrieve(source, destination):
3285 """urllib is broken for SSL connections via a proxy therefore we
3286 can't use urllib.urlretrieve()."""
3287 with open(destination, 'w') as f:
3288 f.write(urllib2.urlopen(source).read())
3289
3290
ukai@chromium.org712d6102013-11-27 00:52:58 +00003291def hasSheBang(fname):
3292 """Checks fname is a #! script."""
3293 with open(fname) as f:
3294 return f.read(2).startswith('#!')
3295
3296
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003297# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3298def DownloadHooks(*args, **kwargs):
3299 pass
3300
3301
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003302def DownloadGerritHook(force):
3303 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003304
3305 Args:
3306 force: True to update hooks. False to install hooks if not present.
3307 """
3308 if not settings.GetIsGerrit():
3309 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003310 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003311 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3312 if not os.access(dst, os.X_OK):
3313 if os.path.exists(dst):
3314 if not force:
3315 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003316 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003317 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003318 if not hasSheBang(dst):
3319 DieWithError('Not a script: %s\n'
3320 'You need to download from\n%s\n'
3321 'into .git/hooks/commit-msg and '
3322 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003323 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3324 except Exception:
3325 if os.path.exists(dst):
3326 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003327 DieWithError('\nFailed to download hooks.\n'
3328 'You need to download from\n%s\n'
3329 'into .git/hooks/commit-msg and '
3330 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003331
3332
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003333
3334def GetRietveldCodereviewSettingsInteractively():
3335 """Prompt the user for settings."""
3336 server = settings.GetDefaultServerUrl(error_ok=True)
3337 prompt = 'Rietveld server (host[:port])'
3338 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3339 newserver = ask_for_data(prompt + ':')
3340 if not server and not newserver:
3341 newserver = DEFAULT_SERVER
3342 if newserver:
3343 newserver = gclient_utils.UpgradeToHttps(newserver)
3344 if newserver != server:
3345 RunGit(['config', 'rietveld.server', newserver])
3346
3347 def SetProperty(initial, caption, name, is_url):
3348 prompt = caption
3349 if initial:
3350 prompt += ' ("x" to clear) [%s]' % initial
3351 new_val = ask_for_data(prompt + ':')
3352 if new_val == 'x':
3353 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3354 elif new_val:
3355 if is_url:
3356 new_val = gclient_utils.UpgradeToHttps(new_val)
3357 if new_val != initial:
3358 RunGit(['config', 'rietveld.' + name, new_val])
3359
3360 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3361 SetProperty(settings.GetDefaultPrivateFlag(),
3362 'Private flag (rietveld only)', 'private', False)
3363 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3364 'tree-status-url', False)
3365 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3366 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3367 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3368 'run-post-upload-hook', False)
3369
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003370@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003371def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003372 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003373
tandrii5d0a0422016-09-14 06:24:35 -07003374 print('WARNING: git cl config works for Rietveld only')
3375 # TODO(tandrii): remove this once we switch to Gerrit.
3376 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003377 parser.add_option('--activate-update', action='store_true',
3378 help='activate auto-updating [rietveld] section in '
3379 '.git/config')
3380 parser.add_option('--deactivate-update', action='store_true',
3381 help='deactivate auto-updating [rietveld] section in '
3382 '.git/config')
3383 options, args = parser.parse_args(args)
3384
3385 if options.deactivate_update:
3386 RunGit(['config', 'rietveld.autoupdate', 'false'])
3387 return
3388
3389 if options.activate_update:
3390 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3391 return
3392
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003393 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003394 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003395 return 0
3396
3397 url = args[0]
3398 if not url.endswith('codereview.settings'):
3399 url = os.path.join(url, 'codereview.settings')
3400
3401 # Load code review settings and download hooks (if available).
3402 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3403 return 0
3404
3405
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003406def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003407 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003408 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3409 branch = ShortBranchName(branchref)
3410 _, args = parser.parse_args(args)
3411 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003412 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003413 return RunGit(['config', 'branch.%s.base-url' % branch],
3414 error_ok=False).strip()
3415 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003416 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003417 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3418 error_ok=False).strip()
3419
3420
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003421def color_for_status(status):
3422 """Maps a Changelist status to color, for CMDstatus and other tools."""
3423 return {
3424 'unsent': Fore.RED,
3425 'waiting': Fore.BLUE,
3426 'reply': Fore.YELLOW,
3427 'lgtm': Fore.GREEN,
3428 'commit': Fore.MAGENTA,
3429 'closed': Fore.CYAN,
3430 'error': Fore.WHITE,
3431 }.get(status, Fore.WHITE)
3432
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003433
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003434def get_cl_statuses(changes, fine_grained, max_processes=None):
3435 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003436
3437 If fine_grained is true, this will fetch CL statuses from the server.
3438 Otherwise, simply indicate if there's a matching url for the given branches.
3439
3440 If max_processes is specified, it is used as the maximum number of processes
3441 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3442 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003443
3444 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003445 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003446 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003447 upload.verbosity = 0
3448
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003449 if not changes:
3450 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003451
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003452 if not fine_grained:
3453 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003454 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003455 for cl in changes:
3456 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003457 return
3458
3459 # First, sort out authentication issues.
3460 logging.debug('ensuring credentials exist')
3461 for cl in changes:
3462 cl.EnsureAuthenticated(force=False, refresh=True)
3463
3464 def fetch(cl):
3465 try:
3466 return (cl, cl.GetStatus())
3467 except:
3468 # See http://crbug.com/629863.
3469 logging.exception('failed to fetch status for %s:', cl)
3470 raise
3471
3472 threads_count = len(changes)
3473 if max_processes:
3474 threads_count = max(1, min(threads_count, max_processes))
3475 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3476
3477 pool = ThreadPool(threads_count)
3478 fetched_cls = set()
3479 try:
3480 it = pool.imap_unordered(fetch, changes).__iter__()
3481 while True:
3482 try:
3483 cl, status = it.next(timeout=5)
3484 except multiprocessing.TimeoutError:
3485 break
3486 fetched_cls.add(cl)
3487 yield cl, status
3488 finally:
3489 pool.close()
3490
3491 # Add any branches that failed to fetch.
3492 for cl in set(changes) - fetched_cls:
3493 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003494
rmistry@google.com2dd99862015-06-22 12:22:18 +00003495
3496def upload_branch_deps(cl, args):
3497 """Uploads CLs of local branches that are dependents of the current branch.
3498
3499 If the local branch dependency tree looks like:
3500 test1 -> test2.1 -> test3.1
3501 -> test3.2
3502 -> test2.2 -> test3.3
3503
3504 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3505 run on the dependent branches in this order:
3506 test2.1, test3.1, test3.2, test2.2, test3.3
3507
3508 Note: This function does not rebase your local dependent branches. Use it when
3509 you make a change to the parent branch that will not conflict with its
3510 dependent branches, and you would like their dependencies updated in
3511 Rietveld.
3512 """
3513 if git_common.is_dirty_git_tree('upload-branch-deps'):
3514 return 1
3515
3516 root_branch = cl.GetBranch()
3517 if root_branch is None:
3518 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3519 'Get on a branch!')
3520 if not cl.GetIssue() or not cl.GetPatchset():
3521 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3522 'patchset dependencies without an uploaded CL.')
3523
3524 branches = RunGit(['for-each-ref',
3525 '--format=%(refname:short) %(upstream:short)',
3526 'refs/heads'])
3527 if not branches:
3528 print('No local branches found.')
3529 return 0
3530
3531 # Create a dictionary of all local branches to the branches that are dependent
3532 # on it.
3533 tracked_to_dependents = collections.defaultdict(list)
3534 for b in branches.splitlines():
3535 tokens = b.split()
3536 if len(tokens) == 2:
3537 branch_name, tracked = tokens
3538 tracked_to_dependents[tracked].append(branch_name)
3539
vapiera7fbd5a2016-06-16 09:17:49 -07003540 print()
3541 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003542 dependents = []
3543 def traverse_dependents_preorder(branch, padding=''):
3544 dependents_to_process = tracked_to_dependents.get(branch, [])
3545 padding += ' '
3546 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003547 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003548 dependents.append(dependent)
3549 traverse_dependents_preorder(dependent, padding)
3550 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003551 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003552
3553 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003554 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003555 return 0
3556
vapiera7fbd5a2016-06-16 09:17:49 -07003557 print('This command will checkout all dependent branches and run '
3558 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003559 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3560
andybons@chromium.org962f9462016-02-03 20:00:42 +00003561 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003562 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003563 args.extend(['-t', 'Updated patchset dependency'])
3564
rmistry@google.com2dd99862015-06-22 12:22:18 +00003565 # Record all dependents that failed to upload.
3566 failures = {}
3567 # Go through all dependents, checkout the branch and upload.
3568 try:
3569 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003570 print()
3571 print('--------------------------------------')
3572 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003573 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003574 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003575 try:
3576 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003577 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003578 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003579 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003580 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003581 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003582 finally:
3583 # Swap back to the original root branch.
3584 RunGit(['checkout', '-q', root_branch])
3585
vapiera7fbd5a2016-06-16 09:17:49 -07003586 print()
3587 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003588 for dependent_branch in dependents:
3589 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003590 print(' %s : %s' % (dependent_branch, upload_status))
3591 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003592
3593 return 0
3594
3595
kmarshall3bff56b2016-06-06 18:31:47 -07003596def CMDarchive(parser, args):
3597 """Archives and deletes branches associated with closed changelists."""
3598 parser.add_option(
3599 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003600 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003601 parser.add_option(
3602 '-f', '--force', action='store_true',
3603 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003604 parser.add_option(
3605 '-d', '--dry-run', action='store_true',
3606 help='Skip the branch tagging and removal steps.')
3607 parser.add_option(
3608 '-t', '--notags', action='store_true',
3609 help='Do not tag archived branches. '
3610 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003611
3612 auth.add_auth_options(parser)
3613 options, args = parser.parse_args(args)
3614 if args:
3615 parser.error('Unsupported args: %s' % ' '.join(args))
3616 auth_config = auth.extract_auth_config_from_options(options)
3617
3618 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3619 if not branches:
3620 return 0
3621
vapiera7fbd5a2016-06-16 09:17:49 -07003622 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003623 changes = [Changelist(branchref=b, auth_config=auth_config)
3624 for b in branches.splitlines()]
3625 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3626 statuses = get_cl_statuses(changes,
3627 fine_grained=True,
3628 max_processes=options.maxjobs)
3629 proposal = [(cl.GetBranch(),
3630 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3631 for cl, status in statuses
3632 if status == 'closed']
3633 proposal.sort()
3634
3635 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003636 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003637 return 0
3638
3639 current_branch = GetCurrentBranch()
3640
vapiera7fbd5a2016-06-16 09:17:49 -07003641 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003642 if options.notags:
3643 for next_item in proposal:
3644 print(' ' + next_item[0])
3645 else:
3646 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3647 for next_item in proposal:
3648 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003649
kmarshall9249e012016-08-23 12:02:16 -07003650 # Quit now on precondition failure or if instructed by the user, either
3651 # via an interactive prompt or by command line flags.
3652 if options.dry_run:
3653 print('\nNo changes were made (dry run).\n')
3654 return 0
3655 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003656 print('You are currently on a branch \'%s\' which is associated with a '
3657 'closed codereview issue, so archive cannot proceed. Please '
3658 'checkout another branch and run this command again.' %
3659 current_branch)
3660 return 1
kmarshall9249e012016-08-23 12:02:16 -07003661 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003662 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3663 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003664 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003665 return 1
3666
3667 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003668 if not options.notags:
3669 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003670 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003671
vapiera7fbd5a2016-06-16 09:17:49 -07003672 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003673
3674 return 0
3675
3676
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003677def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003678 """Show status of changelists.
3679
3680 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003681 - Red not sent for review or broken
3682 - Blue waiting for review
3683 - Yellow waiting for you to reply to review
3684 - Green LGTM'ed
3685 - Magenta in the commit queue
3686 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003687
3688 Also see 'git cl comments'.
3689 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003690 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003691 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003692 parser.add_option('-f', '--fast', action='store_true',
3693 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003694 parser.add_option(
3695 '-j', '--maxjobs', action='store', type=int,
3696 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003697
3698 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003699 _add_codereview_issue_select_options(
3700 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003701 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003702 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003703 if args:
3704 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003705 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003706
iannuccie53c9352016-08-17 14:40:40 -07003707 if options.issue is not None and not options.field:
3708 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003709
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003710 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003711 cl = Changelist(auth_config=auth_config, issue=options.issue,
3712 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003713 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003714 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003715 elif options.field == 'id':
3716 issueid = cl.GetIssue()
3717 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003718 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003719 elif options.field == 'patch':
3720 patchset = cl.GetPatchset()
3721 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003722 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003723 elif options.field == 'status':
3724 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003725 elif options.field == 'url':
3726 url = cl.GetIssueURL()
3727 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003728 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003729 return 0
3730
3731 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3732 if not branches:
3733 print('No local branch found.')
3734 return 0
3735
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003736 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003737 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003738 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003739 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003740 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003741 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003742 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003743
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003744 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003745 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3746 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3747 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003748 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003749 c, status = output.next()
3750 branch_statuses[c.GetBranch()] = status
3751 status = branch_statuses.pop(branch)
3752 url = cl.GetIssueURL()
3753 if url and (not status or status == 'error'):
3754 # The issue probably doesn't exist anymore.
3755 url += ' (broken)'
3756
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003757 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003758 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003759 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003760 color = ''
3761 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003762 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003763 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003764 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003765 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003766
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003767
3768 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003769 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003770 print('Current branch: %s' % branch)
3771 for cl in changes:
3772 if cl.GetBranch() == branch:
3773 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003774 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003775 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003776 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003777 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003778 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003779 print('Issue description:')
3780 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003781 return 0
3782
3783
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003784def colorize_CMDstatus_doc():
3785 """To be called once in main() to add colors to git cl status help."""
3786 colors = [i for i in dir(Fore) if i[0].isupper()]
3787
3788 def colorize_line(line):
3789 for color in colors:
3790 if color in line.upper():
3791 # Extract whitespaces first and the leading '-'.
3792 indent = len(line) - len(line.lstrip(' ')) + 1
3793 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3794 return line
3795
3796 lines = CMDstatus.__doc__.splitlines()
3797 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3798
3799
phajdan.jre328cf92016-08-22 04:12:17 -07003800def write_json(path, contents):
3801 with open(path, 'w') as f:
3802 json.dump(contents, f)
3803
3804
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003805@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003807 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003808
3809 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003810 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003811 parser.add_option('-r', '--reverse', action='store_true',
3812 help='Lookup the branch(es) for the specified issues. If '
3813 'no issues are specified, all branches with mapped '
3814 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003815 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003816 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003817 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003818 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003819
dnj@chromium.org406c4402015-03-03 17:22:28 +00003820 if options.reverse:
3821 branches = RunGit(['for-each-ref', 'refs/heads',
3822 '--format=%(refname:short)']).splitlines()
3823
3824 # Reverse issue lookup.
3825 issue_branch_map = {}
3826 for branch in branches:
3827 cl = Changelist(branchref=branch)
3828 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3829 if not args:
3830 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003831 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003832 for issue in args:
3833 if not issue:
3834 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003835 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003836 print('Branch for issue number %s: %s' % (
3837 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003838 if options.json:
3839 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003840 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003841 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003842 if len(args) > 0:
3843 try:
3844 issue = int(args[0])
3845 except ValueError:
3846 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003847 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003848 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003849 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003850 if options.json:
3851 write_json(options.json, {
3852 'issue': cl.GetIssue(),
3853 'issue_url': cl.GetIssueURL(),
3854 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003855 return 0
3856
3857
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003858def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003859 """Shows or posts review comments for any changelist."""
3860 parser.add_option('-a', '--add-comment', dest='comment',
3861 help='comment to add to an issue')
3862 parser.add_option('-i', dest='issue',
3863 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003864 parser.add_option('-j', '--json-file',
3865 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003866 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003867 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003868 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003869
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003870 issue = None
3871 if options.issue:
3872 try:
3873 issue = int(options.issue)
3874 except ValueError:
3875 DieWithError('A review issue id is expected to be a number')
3876
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003877 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003878
3879 if options.comment:
3880 cl.AddComment(options.comment)
3881 return 0
3882
3883 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003884 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003885 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003886 summary.append({
3887 'date': message['date'],
3888 'lgtm': False,
3889 'message': message['text'],
3890 'not_lgtm': False,
3891 'sender': message['sender'],
3892 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003893 if message['disapproval']:
3894 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003895 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003896 elif message['approval']:
3897 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003898 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003899 elif message['sender'] == data['owner_email']:
3900 color = Fore.MAGENTA
3901 else:
3902 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003903 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003904 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003905 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003906 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003907 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003908 if options.json_file:
3909 with open(options.json_file, 'wb') as f:
3910 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003911 return 0
3912
3913
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003914@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003915def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003916 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003917 parser.add_option('-d', '--display', action='store_true',
3918 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003919 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003920 help='New description to set for this issue (- for stdin, '
3921 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003922 parser.add_option('-f', '--force', action='store_true',
3923 help='Delete any unpublished Gerrit edits for this issue '
3924 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003925
3926 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003927 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003928 options, args = parser.parse_args(args)
3929 _process_codereview_select_options(parser, options)
3930
3931 target_issue = None
3932 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003933 target_issue = ParseIssueNumberArgument(args[0])
3934 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003935 parser.print_help()
3936 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003937
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003938 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003939
martiniss6eda05f2016-06-30 10:18:35 -07003940 kwargs = {
3941 'auth_config': auth_config,
3942 'codereview': options.forced_codereview,
3943 }
3944 if target_issue:
3945 kwargs['issue'] = target_issue.issue
3946 if options.forced_codereview == 'rietveld':
3947 kwargs['rietveld_server'] = target_issue.hostname
3948
3949 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003950
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003951 if not cl.GetIssue():
3952 DieWithError('This branch has no associated changelist.')
3953 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003954
smut@google.com34fb6b12015-07-13 20:03:26 +00003955 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003956 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003957 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003958
3959 if options.new_description:
3960 text = options.new_description
3961 if text == '-':
3962 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003963 elif text == '+':
3964 base_branch = cl.GetCommonAncestorWithUpstream()
3965 change = cl.GetChange(base_branch, None, local_description=True)
3966 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003967
3968 description.set_description(text)
3969 else:
3970 description.prompt()
3971
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003972 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003973 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003974 return 0
3975
3976
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003977def CreateDescriptionFromLog(args):
3978 """Pulls out the commit log to use as a base for the CL description."""
3979 log_args = []
3980 if len(args) == 1 and not args[0].endswith('.'):
3981 log_args = [args[0] + '..']
3982 elif len(args) == 1 and args[0].endswith('...'):
3983 log_args = [args[0][:-1]]
3984 elif len(args) == 2:
3985 log_args = [args[0] + '..' + args[1]]
3986 else:
3987 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003988 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003989
3990
thestig@chromium.org44202a22014-03-11 19:22:18 +00003991def CMDlint(parser, args):
3992 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003993 parser.add_option('--filter', action='append', metavar='-x,+y',
3994 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003995 auth.add_auth_options(parser)
3996 options, args = parser.parse_args(args)
3997 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003998
3999 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004000 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004001 try:
4002 import cpplint
4003 import cpplint_chromium
4004 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004005 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004006 return 1
4007
4008 # Change the current working directory before calling lint so that it
4009 # shows the correct base.
4010 previous_cwd = os.getcwd()
4011 os.chdir(settings.GetRoot())
4012 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004013 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004014 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4015 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004016 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004017 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004018 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004019
4020 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004021 command = args + files
4022 if options.filter:
4023 command = ['--filter=' + ','.join(options.filter)] + command
4024 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004025
4026 white_regex = re.compile(settings.GetLintRegex())
4027 black_regex = re.compile(settings.GetLintIgnoreRegex())
4028 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4029 for filename in filenames:
4030 if white_regex.match(filename):
4031 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004032 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004033 else:
4034 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4035 extra_check_functions)
4036 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004037 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004038 finally:
4039 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004040 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004041 if cpplint._cpplint_state.error_count != 0:
4042 return 1
4043 return 0
4044
4045
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004046def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004047 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004048 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004049 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004050 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004051 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004052 auth.add_auth_options(parser)
4053 options, args = parser.parse_args(args)
4054 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004055
sbc@chromium.org71437c02015-04-09 19:29:40 +00004056 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004057 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004058 return 1
4059
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004060 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004061 if args:
4062 base_branch = args[0]
4063 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004064 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004065 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004066
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004067 cl.RunHook(
4068 committing=not options.upload,
4069 may_prompt=False,
4070 verbose=options.verbose,
4071 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004072 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004073
4074
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004075def GenerateGerritChangeId(message):
4076 """Returns Ixxxxxx...xxx change id.
4077
4078 Works the same way as
4079 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4080 but can be called on demand on all platforms.
4081
4082 The basic idea is to generate git hash of a state of the tree, original commit
4083 message, author/committer info and timestamps.
4084 """
4085 lines = []
4086 tree_hash = RunGitSilent(['write-tree'])
4087 lines.append('tree %s' % tree_hash.strip())
4088 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4089 if code == 0:
4090 lines.append('parent %s' % parent.strip())
4091 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4092 lines.append('author %s' % author.strip())
4093 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4094 lines.append('committer %s' % committer.strip())
4095 lines.append('')
4096 # Note: Gerrit's commit-hook actually cleans message of some lines and
4097 # whitespace. This code is not doing this, but it clearly won't decrease
4098 # entropy.
4099 lines.append(message)
4100 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4101 stdin='\n'.join(lines))
4102 return 'I%s' % change_hash.strip()
4103
4104
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004105def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004106 """Computes the remote branch ref to use for the CL.
4107
4108 Args:
4109 remote (str): The git remote for the CL.
4110 remote_branch (str): The git remote branch for the CL.
4111 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004112 """
4113 if not (remote and remote_branch):
4114 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004115
wittman@chromium.org455dc922015-01-26 20:15:50 +00004116 if target_branch:
4117 # Cannonicalize branch references to the equivalent local full symbolic
4118 # refs, which are then translated into the remote full symbolic refs
4119 # below.
4120 if '/' not in target_branch:
4121 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4122 else:
4123 prefix_replacements = (
4124 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4125 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4126 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4127 )
4128 match = None
4129 for regex, replacement in prefix_replacements:
4130 match = re.search(regex, target_branch)
4131 if match:
4132 remote_branch = target_branch.replace(match.group(0), replacement)
4133 break
4134 if not match:
4135 # This is a branch path but not one we recognize; use as-is.
4136 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004137 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4138 # Handle the refs that need to land in different refs.
4139 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004140
wittman@chromium.org455dc922015-01-26 20:15:50 +00004141 # Create the true path to the remote branch.
4142 # Does the following translation:
4143 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4144 # * refs/remotes/origin/master -> refs/heads/master
4145 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4146 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4147 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4148 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4149 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4150 'refs/heads/')
4151 elif remote_branch.startswith('refs/remotes/branch-heads'):
4152 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004153
wittman@chromium.org455dc922015-01-26 20:15:50 +00004154 return remote_branch
4155
4156
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004157def cleanup_list(l):
4158 """Fixes a list so that comma separated items are put as individual items.
4159
4160 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4161 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4162 """
4163 items = sum((i.split(',') for i in l), [])
4164 stripped_items = (i.strip() for i in items)
4165 return sorted(filter(None, stripped_items))
4166
4167
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004168@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004169def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004170 """Uploads the current changelist to codereview.
4171
4172 Can skip dependency patchset uploads for a branch by running:
4173 git config branch.branch_name.skip-deps-uploads True
4174 To unset run:
4175 git config --unset branch.branch_name.skip-deps-uploads
4176 Can also set the above globally by using the --global flag.
4177 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004178 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4179 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004180 parser.add_option('--bypass-watchlists', action='store_true',
4181 dest='bypass_watchlists',
4182 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004183 parser.add_option('-f', action='store_true', dest='force',
4184 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004185 parser.add_option('--message', '-m', dest='message',
4186 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004187 parser.add_option('-b', '--bug',
4188 help='pre-populate the bug number(s) for this issue. '
4189 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004190 parser.add_option('--message-file', dest='message_file',
4191 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004192 parser.add_option('--title', '-t', dest='title',
4193 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004194 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004195 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004196 help='reviewer email addresses')
4197 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004198 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004199 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004200 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004201 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004202 parser.add_option('--emulate_svn_auto_props',
4203 '--emulate-svn-auto-props',
4204 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004205 dest="emulate_svn_auto_props",
4206 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004207 parser.add_option('-c', '--use-commit-queue', action='store_true',
4208 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004209 parser.add_option('--private', action='store_true',
4210 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004211 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004212 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004213 metavar='TARGET',
4214 help='Apply CL to remote ref TARGET. ' +
4215 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004216 parser.add_option('--squash', action='store_true',
4217 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004218 parser.add_option('--no-squash', action='store_true',
4219 help='Don\'t squash multiple commits into one ' +
4220 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004221 parser.add_option('--topic', default=None,
4222 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004223 parser.add_option('--email', default=None,
4224 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004225 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4226 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004227 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4228 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004229 help='Send the patchset to do a CQ dry run right after '
4230 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004231 parser.add_option('--dependencies', action='store_true',
4232 help='Uploads CLs of all the local branches that depend on '
4233 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004234
rmistry@google.com2dd99862015-06-22 12:22:18 +00004235 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004236 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004237 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004238 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004239 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004240 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004241 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004242
sbc@chromium.org71437c02015-04-09 19:29:40 +00004243 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004244 return 1
4245
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004246 options.reviewers = cleanup_list(options.reviewers)
4247 options.cc = cleanup_list(options.cc)
4248
tandriib80458a2016-06-23 12:20:07 -07004249 if options.message_file:
4250 if options.message:
4251 parser.error('only one of --message and --message-file allowed.')
4252 options.message = gclient_utils.FileRead(options.message_file)
4253 options.message_file = None
4254
tandrii4d0545a2016-07-06 03:56:49 -07004255 if options.cq_dry_run and options.use_commit_queue:
4256 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4257
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004258 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4259 settings.GetIsGerrit()
4260
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004261 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004262 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004263
4264
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004265@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004266def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004267 """DEPRECATED: Used to commit the current changelist via git-svn."""
4268 message = ('git-cl no longer supports committing to SVN repositories via '
4269 'git-svn. You probably want to use `git cl land` instead.')
4270 print(message)
4271 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004272
4273
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004274@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004275def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004276 """Commits the current changelist via git.
4277
4278 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4279 upstream and closes the issue automatically and atomically.
4280
4281 Otherwise (in case of Rietveld):
4282 Squashes branch into a single commit.
4283 Updates commit message with metadata (e.g. pointer to review).
4284 Pushes the code upstream.
4285 Updates review and closes.
4286 """
4287 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4288 help='bypass upload presubmit hook')
4289 parser.add_option('-m', dest='message',
4290 help="override review description")
4291 parser.add_option('-f', action='store_true', dest='force',
4292 help="force yes to questions (don't prompt)")
4293 parser.add_option('-c', dest='contributor',
4294 help="external contributor for patch (appended to " +
4295 "description and used as author for git). Should be " +
4296 "formatted as 'First Last <email@example.com>'")
4297 add_git_similarity(parser)
4298 auth.add_auth_options(parser)
4299 (options, args) = parser.parse_args(args)
4300 auth_config = auth.extract_auth_config_from_options(options)
4301
4302 cl = Changelist(auth_config=auth_config)
4303
4304 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4305 if cl.IsGerrit():
4306 if options.message:
4307 # This could be implemented, but it requires sending a new patch to
4308 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4309 # Besides, Gerrit has the ability to change the commit message on submit
4310 # automatically, thus there is no need to support this option (so far?).
4311 parser.error('-m MESSAGE option is not supported for Gerrit.')
4312 if options.contributor:
4313 parser.error(
4314 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4315 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4316 'the contributor\'s "name <email>". If you can\'t upload such a '
4317 'commit for review, contact your repository admin and request'
4318 '"Forge-Author" permission.')
4319 if not cl.GetIssue():
4320 DieWithError('You must upload the change first to Gerrit.\n'
4321 ' If you would rather have `git cl land` upload '
4322 'automatically for you, see http://crbug.com/642759')
4323 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4324 options.verbose)
4325
4326 current = cl.GetBranch()
4327 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4328 if remote == '.':
4329 print()
4330 print('Attempting to push branch %r into another local branch!' % current)
4331 print()
4332 print('Either reparent this branch on top of origin/master:')
4333 print(' git reparent-branch --root')
4334 print()
4335 print('OR run `git rebase-update` if you think the parent branch is ')
4336 print('already committed.')
4337 print()
4338 print(' Current parent: %r' % upstream_branch)
4339 return 1
4340
4341 if not args:
4342 # Default to merging against our best guess of the upstream branch.
4343 args = [cl.GetUpstreamBranch()]
4344
4345 if options.contributor:
4346 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4347 print("Please provide contibutor as 'First Last <email@example.com>'")
4348 return 1
4349
4350 base_branch = args[0]
4351
4352 if git_common.is_dirty_git_tree('land'):
4353 return 1
4354
4355 # This rev-list syntax means "show all commits not in my branch that
4356 # are in base_branch".
4357 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4358 base_branch]).splitlines()
4359 if upstream_commits:
4360 print('Base branch "%s" has %d commits '
4361 'not in this branch.' % (base_branch, len(upstream_commits)))
4362 print('Run "git merge %s" before attempting to land.' % base_branch)
4363 return 1
4364
4365 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4366 if not options.bypass_hooks:
4367 author = None
4368 if options.contributor:
4369 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4370 hook_results = cl.RunHook(
4371 committing=True,
4372 may_prompt=not options.force,
4373 verbose=options.verbose,
4374 change=cl.GetChange(merge_base, author))
4375 if not hook_results.should_continue():
4376 return 1
4377
4378 # Check the tree status if the tree status URL is set.
4379 status = GetTreeStatus()
4380 if 'closed' == status:
4381 print('The tree is closed. Please wait for it to reopen. Use '
4382 '"git cl land --bypass-hooks" to commit on a closed tree.')
4383 return 1
4384 elif 'unknown' == status:
4385 print('Unable to determine tree status. Please verify manually and '
4386 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4387 return 1
4388
4389 change_desc = ChangeDescription(options.message)
4390 if not change_desc.description and cl.GetIssue():
4391 change_desc = ChangeDescription(cl.GetDescription())
4392
4393 if not change_desc.description:
4394 if not cl.GetIssue() and options.bypass_hooks:
4395 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4396 else:
4397 print('No description set.')
4398 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4399 return 1
4400
4401 # Keep a separate copy for the commit message, because the commit message
4402 # contains the link to the Rietveld issue, while the Rietveld message contains
4403 # the commit viewvc url.
4404 if cl.GetIssue():
4405 change_desc.update_reviewers(cl.GetApprovingReviewers())
4406
4407 commit_desc = ChangeDescription(change_desc.description)
4408 if cl.GetIssue():
4409 # Xcode won't linkify this URL unless there is a non-whitespace character
4410 # after it. Add a period on a new line to circumvent this. Also add a space
4411 # before the period to make sure that Gitiles continues to correctly resolve
4412 # the URL.
4413 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4414 if options.contributor:
4415 commit_desc.append_footer('Patch from %s.' % options.contributor)
4416
4417 print('Description:')
4418 print(commit_desc.description)
4419
4420 branches = [merge_base, cl.GetBranchRef()]
4421 if not options.force:
4422 print_stats(options.similarity, options.find_copies, branches)
4423
4424 # We want to squash all this branch's commits into one commit with the proper
4425 # description. We do this by doing a "reset --soft" to the base branch (which
4426 # keeps the working copy the same), then landing that.
4427 MERGE_BRANCH = 'git-cl-commit'
4428 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4429 # Delete the branches if they exist.
4430 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4431 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4432 result = RunGitWithCode(showref_cmd)
4433 if result[0] == 0:
4434 RunGit(['branch', '-D', branch])
4435
4436 # We might be in a directory that's present in this branch but not in the
4437 # trunk. Move up to the top of the tree so that git commands that expect a
4438 # valid CWD won't fail after we check out the merge branch.
4439 rel_base_path = settings.GetRelativeRoot()
4440 if rel_base_path:
4441 os.chdir(rel_base_path)
4442
4443 # Stuff our change into the merge branch.
4444 # We wrap in a try...finally block so if anything goes wrong,
4445 # we clean up the branches.
4446 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004447 revision = None
4448 try:
4449 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4450 RunGit(['reset', '--soft', merge_base])
4451 if options.contributor:
4452 RunGit(
4453 [
4454 'commit', '--author', options.contributor,
4455 '-m', commit_desc.description,
4456 ])
4457 else:
4458 RunGit(['commit', '-m', commit_desc.description])
4459
4460 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4461 mirror = settings.GetGitMirror(remote)
4462 if mirror:
4463 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004464 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004465 else:
4466 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004467 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004468 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4469
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004470 if git_numberer_enabled:
4471 # TODO(tandrii): maybe do autorebase + retry on failure
4472 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004473 logging.debug('Adding git number footers')
4474 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4475 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4476 branch)
4477 # Ensure timestamps are monotonically increasing.
4478 timestamp = max(1 + _get_committer_timestamp(merge_base),
4479 _get_committer_timestamp('HEAD'))
4480 _git_amend_head(commit_desc.description, timestamp)
4481 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004482
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004483 retcode, output = RunGitWithCode(
4484 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004485 if retcode == 0:
4486 revision = RunGit(['rev-parse', 'HEAD']).strip()
4487 logging.debug(output)
4488 except: # pylint: disable=bare-except
4489 if _IS_BEING_TESTED:
4490 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4491 + '-' * 30 + '8<' + '-' * 30)
4492 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4493 raise
4494 finally:
4495 # And then swap back to the original branch and clean up.
4496 RunGit(['checkout', '-q', cl.GetBranch()])
4497 RunGit(['branch', '-D', MERGE_BRANCH])
4498
4499 if not revision:
4500 print('Failed to push. If this persists, please file a bug.')
4501 return 1
4502
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004503 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004504 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004505 if viewvc_url and revision:
4506 change_desc.append_footer(
4507 'Committed: %s%s' % (viewvc_url, revision))
4508 elif revision:
4509 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004510 print('Closing issue '
4511 '(you may be prompted for your codereview password)...')
4512 cl.UpdateDescription(change_desc.description)
4513 cl.CloseIssue()
4514 props = cl.GetIssueProperties()
4515 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004516 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4517 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004518 if options.bypass_hooks:
4519 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4520 else:
4521 comment += ' (presubmit successful).'
4522 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4523
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004524 if os.path.isfile(POSTUPSTREAM_HOOK):
4525 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4526
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004527 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004528
4529
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004530@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004531def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004532 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004533 parser.add_option('-b', dest='newbranch',
4534 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004535 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004536 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004537 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4538 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004539 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004540 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004541 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004542 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004543 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004544 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004545
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004546
4547 group = optparse.OptionGroup(
4548 parser,
4549 'Options for continuing work on the current issue uploaded from a '
4550 'different clone (e.g. different machine). Must be used independently '
4551 'from the other options. No issue number should be specified, and the '
4552 'branch must have an issue number associated with it')
4553 group.add_option('--reapply', action='store_true', dest='reapply',
4554 help='Reset the branch and reapply the issue.\n'
4555 'CAUTION: This will undo any local changes in this '
4556 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004557
4558 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004559 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004560 parser.add_option_group(group)
4561
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004562 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004563 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004564 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004565 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004566 auth_config = auth.extract_auth_config_from_options(options)
4567
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004568
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004569 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004570 if options.newbranch:
4571 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004572 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004573 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004574
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004575 cl = Changelist(auth_config=auth_config,
4576 codereview=options.forced_codereview)
4577 if not cl.GetIssue():
4578 parser.error('current branch must have an associated issue')
4579
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004580 upstream = cl.GetUpstreamBranch()
4581 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004582 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004583
4584 RunGit(['reset', '--hard', upstream])
4585 if options.pull:
4586 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004587
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004588 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4589 options.directory)
4590
4591 if len(args) != 1 or not args[0]:
4592 parser.error('Must specify issue number or url')
4593
4594 # We don't want uncommitted changes mixed up with the patch.
4595 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004596 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004597
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004598 if options.newbranch:
4599 if options.force:
4600 RunGit(['branch', '-D', options.newbranch],
4601 stderr=subprocess2.PIPE, error_ok=True)
4602 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004603 elif not GetCurrentBranch():
4604 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004605
4606 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4607
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004608 if cl.IsGerrit():
4609 if options.reject:
4610 parser.error('--reject is not supported with Gerrit codereview.')
4611 if options.nocommit:
4612 parser.error('--nocommit is not supported with Gerrit codereview.')
4613 if options.directory:
4614 parser.error('--directory is not supported with Gerrit codereview.')
4615
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004616 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004617 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004618
4619
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004620def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004621 """Fetches the tree status and returns either 'open', 'closed',
4622 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004623 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004624 if url:
4625 status = urllib2.urlopen(url).read().lower()
4626 if status.find('closed') != -1 or status == '0':
4627 return 'closed'
4628 elif status.find('open') != -1 or status == '1':
4629 return 'open'
4630 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004631 return 'unset'
4632
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004633
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004634def GetTreeStatusReason():
4635 """Fetches the tree status from a json url and returns the message
4636 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004637 url = settings.GetTreeStatusUrl()
4638 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004639 connection = urllib2.urlopen(json_url)
4640 status = json.loads(connection.read())
4641 connection.close()
4642 return status['message']
4643
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004645def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004646 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004647 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004648 status = GetTreeStatus()
4649 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004650 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004651 return 2
4652
vapiera7fbd5a2016-06-16 09:17:49 -07004653 print('The tree is %s' % status)
4654 print()
4655 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004656 if status != 'open':
4657 return 1
4658 return 0
4659
4660
maruel@chromium.org15192402012-09-06 12:38:29 +00004661def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004662 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004663 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004664 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004665 '-b', '--bot', action='append',
4666 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4667 'times to specify multiple builders. ex: '
4668 '"-b win_rel -b win_layout". See '
4669 'the try server waterfall for the builders name and the tests '
4670 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004671 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004672 '-B', '--bucket', default='',
4673 help=('Buildbucket bucket to send the try requests.'))
4674 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004675 '-m', '--master', default='',
4676 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004677 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004678 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004679 help='Revision to use for the try job; default: the revision will '
4680 'be determined by the try recipe that builder runs, which usually '
4681 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004682 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004683 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004684 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004685 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004686 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004687 '--project',
4688 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004689 'in recipe to determine to which repository or directory to '
4690 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004691 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004692 '-p', '--property', dest='properties', action='append', default=[],
4693 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004694 'key2=value2 etc. The value will be treated as '
4695 'json if decodable, or as string otherwise. '
4696 'NOTE: using this may make your try job not usable for CQ, '
4697 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004698 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004699 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4700 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004701 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004702 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004703 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004704 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004705
machenbach@chromium.org45453142015-09-15 08:45:22 +00004706 # Make sure that all properties are prop=value pairs.
4707 bad_params = [x for x in options.properties if '=' not in x]
4708 if bad_params:
4709 parser.error('Got properties with missing "=": %s' % bad_params)
4710
maruel@chromium.org15192402012-09-06 12:38:29 +00004711 if args:
4712 parser.error('Unknown arguments: %s' % args)
4713
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004714 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004715 if not cl.GetIssue():
4716 parser.error('Need to upload first')
4717
tandriie113dfd2016-10-11 10:20:12 -07004718 error_message = cl.CannotTriggerTryJobReason()
4719 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004720 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004721
borenet6c0efe62016-10-19 08:13:29 -07004722 if options.bucket and options.master:
4723 parser.error('Only one of --bucket and --master may be used.')
4724
qyearsley1fdfcb62016-10-24 13:22:03 -07004725 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004726
qyearsleydd49f942016-10-28 11:57:22 -07004727 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4728 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004729 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004730 if options.verbose:
4731 print('git cl try with no bots now defaults to CQ Dry Run.')
4732 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004733
borenet6c0efe62016-10-19 08:13:29 -07004734 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004735 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004736 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004737 'of bot requires an initial job from a parent (usually a builder). '
4738 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004739 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004740 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004741
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004742 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004743 # TODO(tandrii): Checking local patchset against remote patchset is only
4744 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4745 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004746 print('Warning: Codereview server has newer patchsets (%s) than most '
4747 'recent upload from local checkout (%s). Did a previous upload '
4748 'fail?\n'
4749 'By default, git cl try uses the latest patchset from '
4750 'codereview, continuing to use patchset %s.\n' %
4751 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004752
tandrii568043b2016-10-11 07:49:18 -07004753 try:
borenet6c0efe62016-10-19 08:13:29 -07004754 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4755 patchset)
tandrii568043b2016-10-11 07:49:18 -07004756 except BuildbucketResponseException as ex:
4757 print('ERROR: %s' % ex)
4758 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004759 return 0
4760
4761
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004762def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004763 """Prints info about try jobs associated with current CL."""
4764 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004765 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004766 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004767 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004768 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004769 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004770 '--color', action='store_true', default=setup_color.IS_TTY,
4771 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004772 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004773 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4774 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004775 group.add_option(
4776 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004777 parser.add_option_group(group)
4778 auth.add_auth_options(parser)
4779 options, args = parser.parse_args(args)
4780 if args:
4781 parser.error('Unrecognized args: %s' % ' '.join(args))
4782
4783 auth_config = auth.extract_auth_config_from_options(options)
4784 cl = Changelist(auth_config=auth_config)
4785 if not cl.GetIssue():
4786 parser.error('Need to upload first')
4787
tandrii221ab252016-10-06 08:12:04 -07004788 patchset = options.patchset
4789 if not patchset:
4790 patchset = cl.GetMostRecentPatchset()
4791 if not patchset:
4792 parser.error('Codereview doesn\'t know about issue %s. '
4793 'No access to issue or wrong issue number?\n'
4794 'Either upload first, or pass --patchset explicitely' %
4795 cl.GetIssue())
4796
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004797 # TODO(tandrii): Checking local patchset against remote patchset is only
4798 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4799 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004800 print('Warning: Codereview server has newer patchsets (%s) than most '
4801 'recent upload from local checkout (%s). Did a previous upload '
4802 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004803 'By default, git cl try-results uses the latest patchset from '
4804 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004805 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004806 try:
tandrii221ab252016-10-06 08:12:04 -07004807 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004808 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004809 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004810 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004811 if options.json:
4812 write_try_results_json(options.json, jobs)
4813 else:
4814 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004815 return 0
4816
4817
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004818@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004819def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004820 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004821 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004822 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004823 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004825 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004826 if args:
4827 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004828 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004829 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004830 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004831 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004832
4833 # Clear configured merge-base, if there is one.
4834 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004835 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004836 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004837 return 0
4838
4839
thestig@chromium.org00858c82013-12-02 23:08:03 +00004840def CMDweb(parser, args):
4841 """Opens the current CL in the web browser."""
4842 _, args = parser.parse_args(args)
4843 if args:
4844 parser.error('Unrecognized args: %s' % ' '.join(args))
4845
4846 issue_url = Changelist().GetIssueURL()
4847 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004848 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004849 return 1
4850
4851 webbrowser.open(issue_url)
4852 return 0
4853
4854
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004855def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004856 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004857 parser.add_option('-d', '--dry-run', action='store_true',
4858 help='trigger in dry run mode')
4859 parser.add_option('-c', '--clear', action='store_true',
4860 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004861 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004862 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004863 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004864 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004865 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004866 if args:
4867 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004868 if options.dry_run and options.clear:
4869 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4870
iannuccie53c9352016-08-17 14:40:40 -07004871 cl = Changelist(auth_config=auth_config, issue=options.issue,
4872 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004873 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004874 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004875 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004876 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004877 state = _CQState.DRY_RUN
4878 else:
4879 state = _CQState.COMMIT
4880 if not cl.GetIssue():
4881 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004882 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004883 return 0
4884
4885
groby@chromium.org411034a2013-02-26 15:12:01 +00004886def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004887 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004888 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004889 auth.add_auth_options(parser)
4890 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004891 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004892 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004893 if args:
4894 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004895 cl = Changelist(auth_config=auth_config, issue=options.issue,
4896 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004897 # Ensure there actually is an issue to close.
4898 cl.GetDescription()
4899 cl.CloseIssue()
4900 return 0
4901
4902
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004903def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004904 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004905 parser.add_option(
4906 '--stat',
4907 action='store_true',
4908 dest='stat',
4909 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004910 auth.add_auth_options(parser)
4911 options, args = parser.parse_args(args)
4912 auth_config = auth.extract_auth_config_from_options(options)
4913 if args:
4914 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004915
4916 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004917 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004918 # Staged changes would be committed along with the patch from last
4919 # upload, hence counted toward the "last upload" side in the final
4920 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004921 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004922 return 1
4923
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004924 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004925 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004926 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004927 if not issue:
4928 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004929 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004930 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004931
4932 # Create a new branch based on the merge-base
4933 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004934 # Clear cached branch in cl object, to avoid overwriting original CL branch
4935 # properties.
4936 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004937 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004938 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004939 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004940 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004941 return rtn
4942
wychen@chromium.org06928532015-02-03 02:11:29 +00004943 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004944 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004945 cmd = ['git', 'diff']
4946 if options.stat:
4947 cmd.append('--stat')
4948 cmd.extend([TMP_BRANCH, branch, '--'])
4949 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004950 finally:
4951 RunGit(['checkout', '-q', branch])
4952 RunGit(['branch', '-D', TMP_BRANCH])
4953
4954 return 0
4955
4956
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004957def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004958 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004959 parser.add_option(
4960 '--no-color',
4961 action='store_true',
4962 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004963 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004964 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004965 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004966
4967 author = RunGit(['config', 'user.email']).strip() or None
4968
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004969 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004970
4971 if args:
4972 if len(args) > 1:
4973 parser.error('Unknown args')
4974 base_branch = args[0]
4975 else:
4976 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004977 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004978
4979 change = cl.GetChange(base_branch, None)
4980 return owners_finder.OwnersFinder(
4981 [f.LocalPath() for f in
4982 cl.GetChange(base_branch, None).AffectedFiles()],
4983 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004984 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004985 disable_color=options.no_color).run()
4986
4987
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004988def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004989 """Generates a diff command."""
4990 # Generate diff for the current branch's changes.
4991 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4992 upstream_commit, '--' ]
4993
4994 if args:
4995 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004996 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004997 diff_cmd.append(arg)
4998 else:
4999 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005000
5001 return diff_cmd
5002
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005003def MatchingFileType(file_name, extensions):
5004 """Returns true if the file name ends with one of the given extensions."""
5005 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005006
enne@chromium.org555cfe42014-01-29 18:21:39 +00005007@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005008def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005009 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005010 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005011 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005012 parser.add_option('--full', action='store_true',
5013 help='Reformat the full content of all touched files')
5014 parser.add_option('--dry-run', action='store_true',
5015 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005016 parser.add_option('--python', action='store_true',
5017 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005018 parser.add_option('--js', action='store_true',
5019 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005020 parser.add_option('--diff', action='store_true',
5021 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005022 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005023
Daniel Chengc55eecf2016-12-30 03:11:02 -08005024 # Normalize any remaining args against the current path, so paths relative to
5025 # the current directory are still resolved as expected.
5026 args = [os.path.join(os.getcwd(), arg) for arg in args]
5027
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005028 # git diff generates paths against the root of the repository. Change
5029 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005030 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005031 if rel_base_path:
5032 os.chdir(rel_base_path)
5033
digit@chromium.org29e47272013-05-17 17:01:46 +00005034 # Grab the merge-base commit, i.e. the upstream commit of the current
5035 # branch when it was created or the last time it was rebased. This is
5036 # to cover the case where the user may have called "git fetch origin",
5037 # moving the origin branch to a newer commit, but hasn't rebased yet.
5038 upstream_commit = None
5039 cl = Changelist()
5040 upstream_branch = cl.GetUpstreamBranch()
5041 if upstream_branch:
5042 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5043 upstream_commit = upstream_commit.strip()
5044
5045 if not upstream_commit:
5046 DieWithError('Could not find base commit for this branch. '
5047 'Are you in detached state?')
5048
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005049 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5050 diff_output = RunGit(changed_files_cmd)
5051 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005052 # Filter out files deleted by this CL
5053 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005054
Christopher Lamc5ba6922017-01-24 11:19:14 +11005055 if opts.js:
5056 CLANG_EXTS.append('.js')
5057
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005058 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5059 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5060 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005061 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005062
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005063 top_dir = os.path.normpath(
5064 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5065
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005066 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5067 # formatted. This is used to block during the presubmit.
5068 return_value = 0
5069
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005070 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005071 # Locate the clang-format binary in the checkout
5072 try:
5073 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005074 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005075 DieWithError(e)
5076
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005077 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005078 cmd = [clang_format_tool]
5079 if not opts.dry_run and not opts.diff:
5080 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005081 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005082 if opts.diff:
5083 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005084 else:
5085 env = os.environ.copy()
5086 env['PATH'] = str(os.path.dirname(clang_format_tool))
5087 try:
5088 script = clang_format.FindClangFormatScriptInChromiumTree(
5089 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005090 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005091 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005092
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005093 cmd = [sys.executable, script, '-p0']
5094 if not opts.dry_run and not opts.diff:
5095 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005096
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005097 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5098 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005099
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005100 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5101 if opts.diff:
5102 sys.stdout.write(stdout)
5103 if opts.dry_run and len(stdout) > 0:
5104 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005105
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005106 # Similar code to above, but using yapf on .py files rather than clang-format
5107 # on C/C++ files
5108 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005109 yapf_tool = gclient_utils.FindExecutable('yapf')
5110 if yapf_tool is None:
5111 DieWithError('yapf not found in PATH')
5112
5113 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005114 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005115 cmd = [yapf_tool]
5116 if not opts.dry_run and not opts.diff:
5117 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005118 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005119 if opts.diff:
5120 sys.stdout.write(stdout)
5121 else:
5122 # TODO(sbc): yapf --lines mode still has some issues.
5123 # https://github.com/google/yapf/issues/154
5124 DieWithError('--python currently only works with --full')
5125
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005126 # Dart's formatter does not have the nice property of only operating on
5127 # modified chunks, so hard code full.
5128 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005129 try:
5130 command = [dart_format.FindDartFmtToolInChromiumTree()]
5131 if not opts.dry_run and not opts.diff:
5132 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005133 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005134
ppi@chromium.org6593d932016-03-03 15:41:15 +00005135 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005136 if opts.dry_run and stdout:
5137 return_value = 2
5138 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005139 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5140 'found in this checkout. Files in other languages are still '
5141 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005142
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005143 # Format GN build files. Always run on full build files for canonical form.
5144 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005145 cmd = ['gn', 'format' ]
5146 if opts.dry_run or opts.diff:
5147 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005148 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005149 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5150 shell=sys.platform == 'win32',
5151 cwd=top_dir)
5152 if opts.dry_run and gn_ret == 2:
5153 return_value = 2 # Not formatted.
5154 elif opts.diff and gn_ret == 2:
5155 # TODO this should compute and print the actual diff.
5156 print("This change has GN build file diff for " + gn_diff_file)
5157 elif gn_ret != 0:
5158 # For non-dry run cases (and non-2 return values for dry-run), a
5159 # nonzero error code indicates a failure, probably because the file
5160 # doesn't parse.
5161 DieWithError("gn format failed on " + gn_diff_file +
5162 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005163
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005164 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005165
5166
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005167@subcommand.usage('<codereview url or issue id>')
5168def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005169 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005170 _, args = parser.parse_args(args)
5171
5172 if len(args) != 1:
5173 parser.print_help()
5174 return 1
5175
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005176 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005177 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005178 parser.print_help()
5179 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005180 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005181
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005182 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005183 output = RunGit(['config', '--local', '--get-regexp',
5184 r'branch\..*\.%s' % issueprefix],
5185 error_ok=True)
5186 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005187 if issue == target_issue:
5188 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005189
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005190 branches = []
5191 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005192 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005193 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005194 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005195 return 1
5196 if len(branches) == 1:
5197 RunGit(['checkout', branches[0]])
5198 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005199 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005200 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005201 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005202 which = raw_input('Choose by index: ')
5203 try:
5204 RunGit(['checkout', branches[int(which)]])
5205 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005206 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005207 return 1
5208
5209 return 0
5210
5211
maruel@chromium.org29404b52014-09-08 22:58:00 +00005212def CMDlol(parser, args):
5213 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005214 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005215 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5216 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5217 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005218 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005219 return 0
5220
5221
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005222class OptionParser(optparse.OptionParser):
5223 """Creates the option parse and add --verbose support."""
5224 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005225 optparse.OptionParser.__init__(
5226 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005227 self.add_option(
5228 '-v', '--verbose', action='count', default=0,
5229 help='Use 2 times for more debugging info')
5230
5231 def parse_args(self, args=None, values=None):
5232 options, args = optparse.OptionParser.parse_args(self, args, values)
5233 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005234 logging.basicConfig(
5235 level=levels[min(options.verbose, len(levels) - 1)],
5236 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5237 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005238 return options, args
5239
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005240
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005241def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005242 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005243 print('\nYour python version %s is unsupported, please upgrade.\n' %
5244 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005245 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005246
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005247 # Reload settings.
5248 global settings
5249 settings = Settings()
5250
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005251 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005252 dispatcher = subcommand.CommandDispatcher(__name__)
5253 try:
5254 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005255 except auth.AuthenticationError as e:
5256 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005257 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005258 if e.code != 500:
5259 raise
5260 DieWithError(
5261 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5262 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005263 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005264
5265
5266if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005267 # These affect sys.stdout so do it outside of main() to simplify mocks in
5268 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005269 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005270 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005271 try:
5272 sys.exit(main(sys.argv[1:]))
5273 except KeyboardInterrupt:
5274 sys.stderr.write('interrupted\n')
5275 sys.exit(1)