blob: a01125df4c767166e1a9a4bd3c6c336e4cda626b [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010017import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000018import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000019import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000021import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import optparse
23import os
24import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000025import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080036 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
skobes6468b902016-10-24 08:45:10 -070044import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080067POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
borenet6c0efe62016-10-19 08:13:29 -070078# Buildbucket master name prefix.
79MASTER_PREFIX = 'master.'
80
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000081# Shortcut since it quickly becomes redundant.
82Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000083
maruel@chromium.orgddd59412011-11-30 14:20:38 +000084# Initialized in main()
85settings = None
86
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010087# Used by tests/git_cl_test.py to add extra logging.
88# Inside the weirdly failing test, add this:
89# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
90# And scroll up to see the strack trace printed.
91_IS_BEING_TESTED = False
92
maruel@chromium.orgddd59412011-11-30 14:20:38 +000093
Christopher Lamf732cd52017-01-24 12:40:11 +110094def DieWithError(message, change_desc=None):
95 if change_desc:
96 SaveDescriptionBackup(change_desc)
97
vapiera7fbd5a2016-06-16 09:17:49 -070098 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 sys.exit(1)
100
101
Christopher Lamf732cd52017-01-24 12:40:11 +1100102def SaveDescriptionBackup(change_desc):
103 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
104 print('\nError after CL description prompt -- saving description to %s\n' %
105 backup_path)
106 backup_file = open(backup_path, 'w')
107 backup_file.write(change_desc.description)
108 backup_file.close()
109
110
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000111def GetNoGitPagerEnv():
112 env = os.environ.copy()
113 # 'cat' is a magical git string that disables pagers on all platforms.
114 env['GIT_PAGER'] = 'cat'
115 return env
116
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000117
bsep@chromium.org627d9002016-04-29 00:00:52 +0000118def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000119 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000120 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000121 except subprocess2.CalledProcessError as e:
122 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000123 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000124 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000125 'Command "%s" failed.\n%s' % (
126 ' '.join(args), error_message or e.stdout or ''))
127 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128
129
130def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000131 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000132 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000135def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700137 if suppress_stderr:
138 stderr = subprocess2.VOID
139 else:
140 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000141 try:
tandrii5d48c322016-08-18 16:19:37 -0700142 (out, _), code = subprocess2.communicate(['git'] + args,
143 env=GetNoGitPagerEnv(),
144 stdout=subprocess2.PIPE,
145 stderr=stderr)
146 return code, out
147 except subprocess2.CalledProcessError as e:
148 logging.debug('Failed running %s', args)
149 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000150
151
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000152def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000153 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000154 return RunGitWithCode(args, suppress_stderr=True)[1]
155
156
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000157def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000158 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000159 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000160 return (version.startswith(prefix) and
161 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162
163
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000164def BranchExists(branch):
165 """Return True if specified branch exists."""
166 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
167 suppress_stderr=True)
168 return not code
169
170
tandrii2a16b952016-10-19 07:09:44 -0700171def time_sleep(seconds):
172 # Use this so that it can be mocked in tests without interfering with python
173 # system machinery.
174 import time # Local import to discourage others from importing time globally.
175 return time.sleep(seconds)
176
177
maruel@chromium.org90541732011-04-01 17:54:18 +0000178def ask_for_data(prompt):
179 try:
180 return raw_input(prompt)
181 except KeyboardInterrupt:
182 # Hide the exception.
183 sys.exit(1)
184
185
tandrii5d48c322016-08-18 16:19:37 -0700186def _git_branch_config_key(branch, key):
187 """Helper method to return Git config key for a branch."""
188 assert branch, 'branch name is required to set git config for it'
189 return 'branch.%s.%s' % (branch, key)
190
191
192def _git_get_branch_config_value(key, default=None, value_type=str,
193 branch=False):
194 """Returns git config value of given or current branch if any.
195
196 Returns default in all other cases.
197 """
198 assert value_type in (int, str, bool)
199 if branch is False: # Distinguishing default arg value from None.
200 branch = GetCurrentBranch()
201
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000202 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700203 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000204
tandrii5d48c322016-08-18 16:19:37 -0700205 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700206 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700207 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700208 # git config also has --int, but apparently git config suffers from integer
209 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700210 args.append(_git_branch_config_key(branch, key))
211 code, out = RunGitWithCode(args)
212 if code == 0:
213 value = out.strip()
214 if value_type == int:
215 return int(value)
216 if value_type == bool:
217 return bool(value.lower() == 'true')
218 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000219 return default
220
221
tandrii5d48c322016-08-18 16:19:37 -0700222def _git_set_branch_config_value(key, value, branch=None, **kwargs):
223 """Sets the value or unsets if it's None of a git branch config.
224
225 Valid, though not necessarily existing, branch must be provided,
226 otherwise currently checked out branch is used.
227 """
228 if not branch:
229 branch = GetCurrentBranch()
230 assert branch, 'a branch name OR currently checked out branch is required'
231 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700232 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700233 if value is None:
234 args.append('--unset')
235 elif isinstance(value, bool):
236 args.append('--bool')
237 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700238 else:
tandrii33a46ff2016-08-23 05:53:40 -0700239 # git config also has --int, but apparently git config suffers from integer
240 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700241 value = str(value)
242 args.append(_git_branch_config_key(branch, key))
243 if value is not None:
244 args.append(value)
245 RunGit(args, **kwargs)
246
247
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100248def _get_committer_timestamp(commit):
249 """Returns unix timestamp as integer of a committer in a commit.
250
251 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
252 """
253 # Git also stores timezone offset, but it only affects visual display,
254 # actual point in time is defined by this timestamp only.
255 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
256
257
258def _git_amend_head(message, committer_timestamp):
259 """Amends commit with new message and desired committer_timestamp.
260
261 Sets committer timezone to UTC.
262 """
263 env = os.environ.copy()
264 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
265 return RunGit(['commit', '--amend', '-m', message], env=env)
266
267
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000268def add_git_similarity(parser):
269 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700270 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000271 help='Sets the percentage that a pair of files need to match in order to'
272 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000273 parser.add_option(
274 '--find-copies', action='store_true',
275 help='Allows git to look for copies.')
276 parser.add_option(
277 '--no-find-copies', action='store_false', dest='find_copies',
278 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000279
280 old_parser_args = parser.parse_args
281 def Parse(args):
282 options, args = old_parser_args(args)
283
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000284 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700285 options.similarity = _git_get_branch_config_value(
286 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000287 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000288 print('Note: Saving similarity of %d%% in git config.'
289 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700290 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000291
iannucci@chromium.org79540052012-10-19 23:15:26 +0000292 options.similarity = max(0, min(options.similarity, 100))
293
294 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700295 options.find_copies = _git_get_branch_config_value(
296 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000297 else:
tandrii5d48c322016-08-18 16:19:37 -0700298 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000299
300 print('Using %d%% similarity for rename/copy detection. '
301 'Override with --similarity.' % options.similarity)
302
303 return options, args
304 parser.parse_args = Parse
305
306
machenbach@chromium.org45453142015-09-15 08:45:22 +0000307def _get_properties_from_options(options):
308 properties = dict(x.split('=', 1) for x in options.properties)
309 for key, val in properties.iteritems():
310 try:
311 properties[key] = json.loads(val)
312 except ValueError:
313 pass # If a value couldn't be evaluated, treat it as a string.
314 return properties
315
316
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000317def _prefix_master(master):
318 """Convert user-specified master name to full master name.
319
320 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
321 name, while the developers always use shortened master name
322 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
323 function does the conversion for buildbucket migration.
324 """
borenet6c0efe62016-10-19 08:13:29 -0700325 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000326 return master
borenet6c0efe62016-10-19 08:13:29 -0700327 return '%s%s' % (MASTER_PREFIX, master)
328
329
330def _unprefix_master(bucket):
331 """Convert bucket name to shortened master name.
332
333 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
334 name, while the developers always use shortened master name
335 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
336 function does the conversion for buildbucket migration.
337 """
338 if bucket.startswith(MASTER_PREFIX):
339 return bucket[len(MASTER_PREFIX):]
340 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000341
342
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000343def _buildbucket_retry(operation_name, http, *args, **kwargs):
344 """Retries requests to buildbucket service and returns parsed json content."""
345 try_count = 0
346 while True:
347 response, content = http.request(*args, **kwargs)
348 try:
349 content_json = json.loads(content)
350 except ValueError:
351 content_json = None
352
353 # Buildbucket could return an error even if status==200.
354 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000355 error = content_json.get('error')
356 if error.get('code') == 403:
357 raise BuildbucketResponseException(
358 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000360 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000361 raise BuildbucketResponseException(msg)
362
363 if response.status == 200:
364 if not content_json:
365 raise BuildbucketResponseException(
366 'Buildbucket returns invalid json content: %s.\n'
367 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
368 content)
369 return content_json
370 if response.status < 500 or try_count >= 2:
371 raise httplib2.HttpLib2Error(content)
372
373 # status >= 500 means transient failures.
374 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700375 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000376 try_count += 1
377 assert False, 'unreachable'
378
379
qyearsley1fdfcb62016-10-24 13:22:03 -0700380def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700381 """Returns a dict mapping bucket names to builders and tests,
382 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700383 """
qyearsleydd49f942016-10-28 11:57:22 -0700384 # If no bots are listed, we try to get a set of builders and tests based
385 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700386 if not options.bot:
387 change = changelist.GetChange(
388 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700389 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700390 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700391 change=change,
392 changed_files=change.LocalPaths(),
393 repository_root=settings.GetRoot(),
394 default_presubmit=None,
395 project=None,
396 verbose=options.verbose,
397 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700398 if masters is None:
399 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100400 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700401
qyearsley1fdfcb62016-10-24 13:22:03 -0700402 if options.bucket:
403 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700404 if options.master:
405 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
qyearsleydd49f942016-10-28 11:57:22 -0700407 # If bots are listed but no master or bucket, then we need to find out
408 # the corresponding master for each bot.
409 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
410 if error_message:
411 option_parser.error(
412 'Tryserver master cannot be found because: %s\n'
413 'Please manually specify the tryserver master, e.g. '
414 '"-m tryserver.chromium.linux".' % error_message)
415 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700416
417
qyearsley123a4682016-10-26 09:12:17 -0700418def _get_bucket_map_for_builders(builders):
419 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700420 map_url = 'https://builders-map.appspot.com/'
421 try:
qyearsley123a4682016-10-26 09:12:17 -0700422 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700423 except urllib2.URLError as e:
424 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
425 (map_url, e))
426 except ValueError as e:
427 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700428 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700429 return None, 'Failed to build master map.'
430
qyearsley123a4682016-10-26 09:12:17 -0700431 bucket_map = {}
432 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700433 masters = builders_map.get(builder, [])
434 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700435 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700436 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700437 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700438 (builder, masters))
439 bucket = _prefix_master(masters[0])
440 bucket_map.setdefault(bucket, {})[builder] = []
441
442 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700443
444
borenet6c0efe62016-10-19 08:13:29 -0700445def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700446 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700447 """Sends a request to Buildbucket to trigger try jobs for a changelist.
448
449 Args:
450 auth_config: AuthConfig for Rietveld.
451 changelist: Changelist that the try jobs are associated with.
452 buckets: A nested dict mapping bucket names to builders to tests.
453 options: Command-line options.
454 """
tandriide281ae2016-10-12 06:02:30 -0700455 assert changelist.GetIssue(), 'CL must be uploaded first'
456 codereview_url = changelist.GetCodereviewServer()
457 assert codereview_url, 'CL must be uploaded first'
458 patchset = patchset or changelist.GetMostRecentPatchset()
459 assert patchset, 'CL must be uploaded first'
460
461 codereview_host = urlparse.urlparse(codereview_url).hostname
462 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000463 http = authenticator.authorize(httplib2.Http())
464 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700465
466 # TODO(tandrii): consider caching Gerrit CL details just like
467 # _RietveldChangelistImpl does, then caching values in these two variables
468 # won't be necessary.
469 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000470
471 buildbucket_put_url = (
472 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000473 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700474 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
475 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
476 hostname=codereview_host,
477 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000478 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700479
480 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
481 shared_parameters_properties['category'] = category
482 if options.clobber:
483 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700484 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700485 if extra_properties:
486 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000487
488 batch_req_body = {'builds': []}
489 print_text = []
490 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700491 for bucket, builders_and_tests in sorted(buckets.iteritems()):
492 print_text.append('Bucket: %s' % bucket)
493 master = None
494 if bucket.startswith(MASTER_PREFIX):
495 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000496 for builder, tests in sorted(builders_and_tests.iteritems()):
497 print_text.append(' %s: %s' % (builder, tests))
498 parameters = {
499 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000500 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700501 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000502 'revision': options.revision,
503 }],
tandrii8c5a3532016-11-04 07:52:02 -0700504 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000505 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000506 if 'presubmit' in builder.lower():
507 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000508 if tests:
509 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700510
511 tags = [
512 'builder:%s' % builder,
513 'buildset:%s' % buildset,
514 'user_agent:git_cl_try',
515 ]
516 if master:
517 parameters['properties']['master'] = master
518 tags.append('master:%s' % master)
519
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000520 batch_req_body['builds'].append(
521 {
522 'bucket': bucket,
523 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000524 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700525 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000526 }
527 )
528
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000529 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700530 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000531 http,
532 buildbucket_put_url,
533 'PUT',
534 body=json.dumps(batch_req_body),
535 headers={'Content-Type': 'application/json'}
536 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000537 print_text.append('To see results here, run: git cl try-results')
538 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700539 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000540
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000541
tandrii221ab252016-10-06 08:12:04 -0700542def fetch_try_jobs(auth_config, changelist, buildbucket_host,
543 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700544 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000545
qyearsley53f48a12016-09-01 10:45:13 -0700546 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000547 """
tandrii221ab252016-10-06 08:12:04 -0700548 assert buildbucket_host
549 assert changelist.GetIssue(), 'CL must be uploaded first'
550 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
551 patchset = patchset or changelist.GetMostRecentPatchset()
552 assert patchset, 'CL must be uploaded first'
553
554 codereview_url = changelist.GetCodereviewServer()
555 codereview_host = urlparse.urlparse(codereview_url).hostname
556 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000557 if authenticator.has_cached_credentials():
558 http = authenticator.authorize(httplib2.Http())
559 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700560 print('Warning: Some results might be missing because %s' %
561 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700562 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000563 http = httplib2.Http()
564
565 http.force_exception_to_status_code = True
566
tandrii221ab252016-10-06 08:12:04 -0700567 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
568 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
569 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700571 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572 params = {'tag': 'buildset:%s' % buildset}
573
574 builds = {}
575 while True:
576 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700577 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000578 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700579 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000580 for build in content.get('builds', []):
581 builds[build['id']] = build
582 if 'next_cursor' in content:
583 params['start_cursor'] = content['next_cursor']
584 else:
585 break
586 return builds
587
588
qyearsleyeab3c042016-08-24 09:18:28 -0700589def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000590 """Prints nicely result of fetch_try_jobs."""
591 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700592 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000593 return
594
595 # Make a copy, because we'll be modifying builds dictionary.
596 builds = builds.copy()
597 builder_names_cache = {}
598
599 def get_builder(b):
600 try:
601 return builder_names_cache[b['id']]
602 except KeyError:
603 try:
604 parameters = json.loads(b['parameters_json'])
605 name = parameters['builder_name']
606 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700607 print('WARNING: failed to get builder name for build %s: %s' % (
608 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000609 name = None
610 builder_names_cache[b['id']] = name
611 return name
612
613 def get_bucket(b):
614 bucket = b['bucket']
615 if bucket.startswith('master.'):
616 return bucket[len('master.'):]
617 return bucket
618
619 if options.print_master:
620 name_fmt = '%%-%ds %%-%ds' % (
621 max(len(str(get_bucket(b))) for b in builds.itervalues()),
622 max(len(str(get_builder(b))) for b in builds.itervalues()))
623 def get_name(b):
624 return name_fmt % (get_bucket(b), get_builder(b))
625 else:
626 name_fmt = '%%-%ds' % (
627 max(len(str(get_builder(b))) for b in builds.itervalues()))
628 def get_name(b):
629 return name_fmt % get_builder(b)
630
631 def sort_key(b):
632 return b['status'], b.get('result'), get_name(b), b.get('url')
633
634 def pop(title, f, color=None, **kwargs):
635 """Pop matching builds from `builds` dict and print them."""
636
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000637 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000638 colorize = str
639 else:
640 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
641
642 result = []
643 for b in builds.values():
644 if all(b.get(k) == v for k, v in kwargs.iteritems()):
645 builds.pop(b['id'])
646 result.append(b)
647 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700648 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000649 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700650 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000651
652 total = len(builds)
653 pop(status='COMPLETED', result='SUCCESS',
654 title='Successes:', color=Fore.GREEN,
655 f=lambda b: (get_name(b), b.get('url')))
656 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
657 title='Infra Failures:', color=Fore.MAGENTA,
658 f=lambda b: (get_name(b), b.get('url')))
659 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
660 title='Failures:', color=Fore.RED,
661 f=lambda b: (get_name(b), b.get('url')))
662 pop(status='COMPLETED', result='CANCELED',
663 title='Canceled:', color=Fore.MAGENTA,
664 f=lambda b: (get_name(b),))
665 pop(status='COMPLETED', result='FAILURE',
666 failure_reason='INVALID_BUILD_DEFINITION',
667 title='Wrong master/builder name:', color=Fore.MAGENTA,
668 f=lambda b: (get_name(b),))
669 pop(status='COMPLETED', result='FAILURE',
670 title='Other failures:',
671 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
672 pop(status='COMPLETED',
673 title='Other finished:',
674 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
675 pop(status='STARTED',
676 title='Started:', color=Fore.YELLOW,
677 f=lambda b: (get_name(b), b.get('url')))
678 pop(status='SCHEDULED',
679 title='Scheduled:',
680 f=lambda b: (get_name(b), 'id=%s' % b['id']))
681 # The last section is just in case buildbucket API changes OR there is a bug.
682 pop(title='Other:',
683 f=lambda b: (get_name(b), 'id=%s' % b['id']))
684 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700685 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000686
687
qyearsley53f48a12016-09-01 10:45:13 -0700688def write_try_results_json(output_file, builds):
689 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
690
691 The input |builds| dict is assumed to be generated by Buildbucket.
692 Buildbucket documentation: http://goo.gl/G0s101
693 """
694
695 def convert_build_dict(build):
696 return {
697 'buildbucket_id': build.get('id'),
698 'status': build.get('status'),
699 'result': build.get('result'),
700 'bucket': build.get('bucket'),
701 'builder_name': json.loads(
702 build.get('parameters_json', '{}')).get('builder_name'),
703 'failure_reason': build.get('failure_reason'),
704 'url': build.get('url'),
705 }
706
707 converted = []
708 for _, build in sorted(builds.items()):
709 converted.append(convert_build_dict(build))
710 write_json(output_file, converted)
711
712
iannucci@chromium.org79540052012-10-19 23:15:26 +0000713def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000714 """Prints statistics about the change to the user."""
715 # --no-ext-diff is broken in some versions of Git, so try to work around
716 # this by overriding the environment (but there is still a problem if the
717 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000718 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000719 if 'GIT_EXTERNAL_DIFF' in env:
720 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000721
722 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800723 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000724 else:
725 similarity_options = ['-M%s' % similarity]
726
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000727 try:
728 stdout = sys.stdout.fileno()
729 except AttributeError:
730 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000731 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000732 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000733 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000734 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000735
736
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000737class BuildbucketResponseException(Exception):
738 pass
739
740
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741class Settings(object):
742 def __init__(self):
743 self.default_server = None
744 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000745 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746 self.tree_status_url = None
747 self.viewvc_url = None
748 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000749 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000750 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000751 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000752 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000753 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000754 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755
756 def LazyUpdateIfNeeded(self):
757 """Updates the settings from a codereview.settings file, if available."""
758 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000759 # The only value that actually changes the behavior is
760 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000761 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000762 error_ok=True
763 ).strip().lower()
764
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000766 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 LoadCodereviewSettingsFromFile(cr_settings_file)
768 self.updated = True
769
770 def GetDefaultServerUrl(self, error_ok=False):
771 if not self.default_server:
772 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000773 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000774 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775 if error_ok:
776 return self.default_server
777 if not self.default_server:
778 error_message = ('Could not find settings file. You must configure '
779 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000780 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000781 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782 return self.default_server
783
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000784 @staticmethod
785 def GetRelativeRoot():
786 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000787
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000789 if self.root is None:
790 self.root = os.path.abspath(self.GetRelativeRoot())
791 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000793 def GetGitMirror(self, remote='origin'):
794 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000795 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000796 if not os.path.isdir(local_url):
797 return None
798 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
799 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100800 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100801 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000802 if mirror.exists():
803 return mirror
804 return None
805
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000806 def GetTreeStatusUrl(self, error_ok=False):
807 if not self.tree_status_url:
808 error_message = ('You must configure your tree status URL by running '
809 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000810 self.tree_status_url = self._GetRietveldConfig(
811 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 return self.tree_status_url
813
814 def GetViewVCUrl(self):
815 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000816 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000817 return self.viewvc_url
818
rmistry@google.com90752582014-01-14 21:04:50 +0000819 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000820 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000821
rmistry@google.com78948ed2015-07-08 23:09:57 +0000822 def GetIsSkipDependencyUpload(self, branch_name):
823 """Returns true if specified branch should skip dep uploads."""
824 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
825 error_ok=True)
826
rmistry@google.com5626a922015-02-26 14:03:30 +0000827 def GetRunPostUploadHook(self):
828 run_post_upload_hook = self._GetRietveldConfig(
829 'run-post-upload-hook', error_ok=True)
830 return run_post_upload_hook == "True"
831
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000832 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000833 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000834
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000835 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000837
ukai@chromium.orge8077812012-02-03 03:41:46 +0000838 def GetIsGerrit(self):
839 """Return true if this repo is assosiated with gerrit code review system."""
840 if self.is_gerrit is None:
841 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
842 return self.is_gerrit
843
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000844 def GetSquashGerritUploads(self):
845 """Return true if uploads to Gerrit should be squashed by default."""
846 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700847 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
848 if self.squash_gerrit_uploads is None:
849 # Default is squash now (http://crbug.com/611892#c23).
850 self.squash_gerrit_uploads = not (
851 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
852 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000853 return self.squash_gerrit_uploads
854
tandriia60502f2016-06-20 02:01:53 -0700855 def GetSquashGerritUploadsOverride(self):
856 """Return True or False if codereview.settings should be overridden.
857
858 Returns None if no override has been defined.
859 """
860 # See also http://crbug.com/611892#c23
861 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
862 error_ok=True).strip()
863 if result == 'true':
864 return True
865 if result == 'false':
866 return False
867 return None
868
tandrii@chromium.org28253532016-04-14 13:46:56 +0000869 def GetGerritSkipEnsureAuthenticated(self):
870 """Return True if EnsureAuthenticated should not be done for Gerrit
871 uploads."""
872 if self.gerrit_skip_ensure_authenticated is None:
873 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000874 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000875 error_ok=True).strip() == 'true')
876 return self.gerrit_skip_ensure_authenticated
877
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000878 def GetGitEditor(self):
879 """Return the editor specified in the git config, or None if none is."""
880 if self.git_editor is None:
881 self.git_editor = self._GetConfig('core.editor', error_ok=True)
882 return self.git_editor or None
883
thestig@chromium.org44202a22014-03-11 19:22:18 +0000884 def GetLintRegex(self):
885 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
886 DEFAULT_LINT_REGEX)
887
888 def GetLintIgnoreRegex(self):
889 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
890 DEFAULT_LINT_IGNORE_REGEX)
891
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000892 def GetProject(self):
893 if not self.project:
894 self.project = self._GetRietveldConfig('project', error_ok=True)
895 return self.project
896
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000897 def _GetRietveldConfig(self, param, **kwargs):
898 return self._GetConfig('rietveld.' + param, **kwargs)
899
rmistry@google.com78948ed2015-07-08 23:09:57 +0000900 def _GetBranchConfig(self, branch_name, param, **kwargs):
901 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
902
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000903 def _GetConfig(self, param, **kwargs):
904 self.LazyUpdateIfNeeded()
905 return RunGit(['config', param], **kwargs).strip()
906
907
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100908@contextlib.contextmanager
909def _get_gerrit_project_config_file(remote_url):
910 """Context manager to fetch and store Gerrit's project.config from
911 refs/meta/config branch and store it in temp file.
912
913 Provides a temporary filename or None if there was error.
914 """
915 error, _ = RunGitWithCode([
916 'fetch', remote_url,
917 '+refs/meta/config:refs/git_cl/meta/config'])
918 if error:
919 # Ref doesn't exist or isn't accessible to current user.
920 print('WARNING: failed to fetch project config for %s: %s' %
921 (remote_url, error))
922 yield None
923 return
924
925 error, project_config_data = RunGitWithCode(
926 ['show', 'refs/git_cl/meta/config:project.config'])
927 if error:
928 print('WARNING: project.config file not found')
929 yield None
930 return
931
932 with gclient_utils.temporary_directory() as tempdir:
933 project_config_file = os.path.join(tempdir, 'project.config')
934 gclient_utils.FileWrite(project_config_file, project_config_data)
935 yield project_config_file
936
937
938def _is_git_numberer_enabled(remote_url, remote_ref):
939 """Returns True if Git Numberer is enabled on this ref."""
940 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100941 KNOWN_PROJECTS_WHITELIST = [
942 'chromium/src',
943 'external/webrtc',
944 'v8/v8',
945 ]
946
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100947 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
948 url_parts = urlparse.urlparse(remote_url)
949 project_name = url_parts.path.lstrip('/').rstrip('git./')
950 for known in KNOWN_PROJECTS_WHITELIST:
951 if project_name.endswith(known):
952 break
953 else:
954 # Early exit to avoid extra fetches for repos that aren't using Git
955 # Numberer.
956 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100957
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100958 with _get_gerrit_project_config_file(remote_url) as project_config_file:
959 if project_config_file is None:
960 # Failed to fetch project.config, which shouldn't happen on open source
961 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100962 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100963 def get_opts(x):
964 code, out = RunGitWithCode(
965 ['config', '-f', project_config_file, '--get-all',
966 'plugin.git-numberer.validate-%s-refglob' % x])
967 if code == 0:
968 return out.strip().splitlines()
969 return []
970 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100971
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100972 logging.info('validator config enabled %s disabled %s refglobs for '
973 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000974
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100975 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100976 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100977 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100978 return True
979 return False
980
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100981 if match_refglobs(disabled):
982 return False
983 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100984
985
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000986def ShortBranchName(branch):
987 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000988 return branch.replace('refs/heads/', '', 1)
989
990
991def GetCurrentBranchRef():
992 """Returns branch ref (e.g., refs/heads/master) or None."""
993 return RunGit(['symbolic-ref', 'HEAD'],
994 stderr=subprocess2.VOID, error_ok=True).strip() or None
995
996
997def GetCurrentBranch():
998 """Returns current branch or None.
999
1000 For refs/heads/* branches, returns just last part. For others, full ref.
1001 """
1002 branchref = GetCurrentBranchRef()
1003 if branchref:
1004 return ShortBranchName(branchref)
1005 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006
1007
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001008class _CQState(object):
1009 """Enum for states of CL with respect to Commit Queue."""
1010 NONE = 'none'
1011 DRY_RUN = 'dry_run'
1012 COMMIT = 'commit'
1013
1014 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1015
1016
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001017class _ParsedIssueNumberArgument(object):
1018 def __init__(self, issue=None, patchset=None, hostname=None):
1019 self.issue = issue
1020 self.patchset = patchset
1021 self.hostname = hostname
1022
1023 @property
1024 def valid(self):
1025 return self.issue is not None
1026
1027
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001028def ParseIssueNumberArgument(arg):
1029 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1030 fail_result = _ParsedIssueNumberArgument()
1031
1032 if arg.isdigit():
1033 return _ParsedIssueNumberArgument(issue=int(arg))
1034 if not arg.startswith('http'):
1035 return fail_result
1036 url = gclient_utils.UpgradeToHttps(arg)
1037 try:
1038 parsed_url = urlparse.urlparse(url)
1039 except ValueError:
1040 return fail_result
1041 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1042 tmp = cls.ParseIssueURL(parsed_url)
1043 if tmp is not None:
1044 return tmp
1045 return fail_result
1046
1047
Aaron Gablea45ee112016-11-22 15:14:38 -08001048class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001049 def __init__(self, issue, url):
1050 self.issue = issue
1051 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001052 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001053
1054 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001055 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001056 self.issue, self.url)
1057
1058
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001060 """Changelist works with one changelist in local branch.
1061
1062 Supports two codereview backends: Rietveld or Gerrit, selected at object
1063 creation.
1064
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001065 Notes:
1066 * Not safe for concurrent multi-{thread,process} use.
1067 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001068 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001069 """
1070
1071 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1072 """Create a new ChangeList instance.
1073
1074 If issue is given, the codereview must be given too.
1075
1076 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1077 Otherwise, it's decided based on current configuration of the local branch,
1078 with default being 'rietveld' for backwards compatibility.
1079 See _load_codereview_impl for more details.
1080
1081 **kwargs will be passed directly to codereview implementation.
1082 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001084 global settings
1085 if not settings:
1086 # Happens when git_cl.py is used as a utility library.
1087 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001088
1089 if issue:
1090 assert codereview, 'codereview must be known, if issue is known'
1091
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092 self.branchref = branchref
1093 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001094 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 self.branch = ShortBranchName(self.branchref)
1096 else:
1097 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001099 self.lookedup_issue = False
1100 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101 self.has_description = False
1102 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001103 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001105 self.cc = None
1106 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001107 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001108
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001109 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001110 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001111 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001112 assert self._codereview_impl
1113 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001114
1115 def _load_codereview_impl(self, codereview=None, **kwargs):
1116 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001117 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1118 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1119 self._codereview = codereview
1120 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001121 return
1122
1123 # Automatic selection based on issue number set for a current branch.
1124 # Rietveld takes precedence over Gerrit.
1125 assert not self.issue
1126 # Whether we find issue or not, we are doing the lookup.
1127 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001128 if self.GetBranch():
1129 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1130 issue = _git_get_branch_config_value(
1131 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1132 if issue:
1133 self._codereview = codereview
1134 self._codereview_impl = cls(self, **kwargs)
1135 self.issue = int(issue)
1136 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001137
1138 # No issue is set for this branch, so decide based on repo-wide settings.
1139 return self._load_codereview_impl(
1140 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1141 **kwargs)
1142
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001143 def IsGerrit(self):
1144 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001145
1146 def GetCCList(self):
1147 """Return the users cc'd on this CL.
1148
agable92bec4f2016-08-24 09:27:27 -07001149 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001150 """
1151 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001152 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153 more_cc = ','.join(self.watchers)
1154 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1155 return self.cc
1156
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001157 def GetCCListWithoutDefault(self):
1158 """Return the users cc'd on this CL excluding default ones."""
1159 if self.cc is None:
1160 self.cc = ','.join(self.watchers)
1161 return self.cc
1162
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001163 def SetWatchers(self, watchers):
1164 """Set the list of email addresses that should be cc'd based on the changed
1165 files in this CL.
1166 """
1167 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168
1169 def GetBranch(self):
1170 """Returns the short branch name, e.g. 'master'."""
1171 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001172 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001173 if not branchref:
1174 return None
1175 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 self.branch = ShortBranchName(self.branchref)
1177 return self.branch
1178
1179 def GetBranchRef(self):
1180 """Returns the full branch name, e.g. 'refs/heads/master'."""
1181 self.GetBranch() # Poke the lazy loader.
1182 return self.branchref
1183
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001184 def ClearBranch(self):
1185 """Clears cached branch data of this object."""
1186 self.branch = self.branchref = None
1187
tandrii5d48c322016-08-18 16:19:37 -07001188 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1189 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1190 kwargs['branch'] = self.GetBranch()
1191 return _git_get_branch_config_value(key, default, **kwargs)
1192
1193 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1194 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1195 assert self.GetBranch(), (
1196 'this CL must have an associated branch to %sset %s%s' %
1197 ('un' if value is None else '',
1198 key,
1199 '' if value is None else ' to %r' % value))
1200 kwargs['branch'] = self.GetBranch()
1201 return _git_set_branch_config_value(key, value, **kwargs)
1202
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001203 @staticmethod
1204 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001205 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 e.g. 'origin', 'refs/heads/master'
1207 """
1208 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001209 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1210
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001212 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001214 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1215 error_ok=True).strip()
1216 if upstream_branch:
1217 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001219 # Else, try to guess the origin remote.
1220 remote_branches = RunGit(['branch', '-r']).split()
1221 if 'origin/master' in remote_branches:
1222 # Fall back on origin/master if it exits.
1223 remote = 'origin'
1224 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001226 DieWithError(
1227 'Unable to determine default branch to diff against.\n'
1228 'Either pass complete "git diff"-style arguments, like\n'
1229 ' git cl upload origin/master\n'
1230 'or verify this branch is set up to track another \n'
1231 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232
1233 return remote, upstream_branch
1234
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001235 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001236 upstream_branch = self.GetUpstreamBranch()
1237 if not BranchExists(upstream_branch):
1238 DieWithError('The upstream for the current branch (%s) does not exist '
1239 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001240 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001241 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243 def GetUpstreamBranch(self):
1244 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001245 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001247 upstream_branch = upstream_branch.replace('refs/heads/',
1248 'refs/remotes/%s/' % remote)
1249 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1250 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 self.upstream_branch = upstream_branch
1252 return self.upstream_branch
1253
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001254 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001255 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001256 remote, branch = None, self.GetBranch()
1257 seen_branches = set()
1258 while branch not in seen_branches:
1259 seen_branches.add(branch)
1260 remote, branch = self.FetchUpstreamTuple(branch)
1261 branch = ShortBranchName(branch)
1262 if remote != '.' or branch.startswith('refs/remotes'):
1263 break
1264 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001265 remotes = RunGit(['remote'], error_ok=True).split()
1266 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001267 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001268 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001269 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001270 logging.warn('Could not determine which remote this change is '
1271 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001272 else:
1273 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001274 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001275 branch = 'HEAD'
1276 if branch.startswith('refs/remotes'):
1277 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001278 elif branch.startswith('refs/branch-heads/'):
1279 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001280 else:
1281 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001282 return self._remote
1283
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001284 def GitSanityChecks(self, upstream_git_obj):
1285 """Checks git repo status and ensures diff is from local commits."""
1286
sbc@chromium.org79706062015-01-14 21:18:12 +00001287 if upstream_git_obj is None:
1288 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001289 print('ERROR: unable to determine current branch (detached HEAD?)',
1290 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001291 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001292 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001293 return False
1294
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001295 # Verify the commit we're diffing against is in our current branch.
1296 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1297 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1298 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001299 print('ERROR: %s is not in the current branch. You may need to rebase '
1300 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 return False
1302
1303 # List the commits inside the diff, and verify they are all local.
1304 commits_in_diff = RunGit(
1305 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1306 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1307 remote_branch = remote_branch.strip()
1308 if code != 0:
1309 _, remote_branch = self.GetRemoteBranch()
1310
1311 commits_in_remote = RunGit(
1312 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1313
1314 common_commits = set(commits_in_diff) & set(commits_in_remote)
1315 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001316 print('ERROR: Your diff contains %d commits already in %s.\n'
1317 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1318 'the diff. If you are using a custom git flow, you can override'
1319 ' the reference used for this check with "git config '
1320 'gitcl.remotebranch <git-ref>".' % (
1321 len(common_commits), remote_branch, upstream_git_obj),
1322 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001323 return False
1324 return True
1325
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001326 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001327 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001328
1329 Returns None if it is not set.
1330 """
tandrii5d48c322016-08-18 16:19:37 -07001331 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001332
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001333 def GetRemoteUrl(self):
1334 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1335
1336 Returns None if there is no remote.
1337 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001338 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001339 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1340
1341 # If URL is pointing to a local directory, it is probably a git cache.
1342 if os.path.isdir(url):
1343 url = RunGit(['config', 'remote.%s.url' % remote],
1344 error_ok=True,
1345 cwd=url).strip()
1346 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001348 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001349 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001350 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001351 self.issue = self._GitGetBranchConfigValue(
1352 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001353 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354 return self.issue
1355
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 def GetIssueURL(self):
1357 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001358 issue = self.GetIssue()
1359 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001360 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001361 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362
1363 def GetDescription(self, pretty=False):
1364 if not self.has_description:
1365 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001366 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367 self.has_description = True
1368 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001369 # Set width to 72 columns + 2 space indent.
1370 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001372 lines = self.description.splitlines()
1373 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 return self.description
1375
1376 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001377 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001378 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001379 self.patchset = self._GitGetBranchConfigValue(
1380 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001381 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382 return self.patchset
1383
1384 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001385 """Set this branch's patchset. If patchset=0, clears the patchset."""
1386 assert self.GetBranch()
1387 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001388 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001389 else:
1390 self.patchset = int(patchset)
1391 self._GitSetBranchConfigValue(
1392 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001394 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001395 """Set this branch's issue. If issue isn't given, clears the issue."""
1396 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001398 issue = int(issue)
1399 self._GitSetBranchConfigValue(
1400 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001401 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001402 codereview_server = self._codereview_impl.GetCodereviewServer()
1403 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001404 self._GitSetBranchConfigValue(
1405 self._codereview_impl.CodereviewServerConfigKey(),
1406 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407 else:
tandrii5d48c322016-08-18 16:19:37 -07001408 # Reset all of these just to be clean.
1409 reset_suffixes = [
1410 'last-upload-hash',
1411 self._codereview_impl.IssueConfigKey(),
1412 self._codereview_impl.PatchsetConfigKey(),
1413 self._codereview_impl.CodereviewServerConfigKey(),
1414 ] + self._PostUnsetIssueProperties()
1415 for prop in reset_suffixes:
1416 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001417 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001418 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419
dnjba1b0f32016-09-02 12:37:42 -07001420 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001421 if not self.GitSanityChecks(upstream_branch):
1422 DieWithError('\nGit sanity check failure')
1423
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001424 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001425 if not root:
1426 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001427 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001428
1429 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001430 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001431 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001432 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001433 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001434 except subprocess2.CalledProcessError:
1435 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001436 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001437 'This branch probably doesn\'t exist anymore. To reset the\n'
1438 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001439 ' git branch --set-upstream-to origin/master %s\n'
1440 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001441 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001442
maruel@chromium.org52424302012-08-29 15:14:30 +00001443 issue = self.GetIssue()
1444 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001445 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001446 description = self.GetDescription()
1447 else:
1448 # If the change was never uploaded, use the log messages of all commits
1449 # up to the branch point, as git cl upload will prefill the description
1450 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001451 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1452 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001453
1454 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001455 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001456 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001457 name,
1458 description,
1459 absroot,
1460 files,
1461 issue,
1462 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001463 author,
1464 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001465
dsansomee2d6fd92016-09-08 00:10:47 -07001466 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001467 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001468 return self._codereview_impl.UpdateDescriptionRemote(
1469 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001470
1471 def RunHook(self, committing, may_prompt, verbose, change):
1472 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1473 try:
1474 return presubmit_support.DoPresubmitChecks(change, committing,
1475 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1476 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001477 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1478 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001479 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001480 DieWithError(
1481 ('%s\nMaybe your depot_tools is out of date?\n'
1482 'If all fails, contact maruel@') % e)
1483
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001484 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1485 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001486 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1487 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001488 else:
1489 # Assume url.
1490 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1491 urlparse.urlparse(issue_arg))
1492 if not parsed_issue_arg or not parsed_issue_arg.valid:
1493 DieWithError('Failed to parse issue argument "%s". '
1494 'Must be an issue number or a valid URL.' % issue_arg)
1495 return self._codereview_impl.CMDPatchWithParsedIssue(
1496 parsed_issue_arg, reject, nocommit, directory)
1497
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001498 def CMDUpload(self, options, git_diff_args, orig_args):
1499 """Uploads a change to codereview."""
1500 if git_diff_args:
1501 # TODO(ukai): is it ok for gerrit case?
1502 base_branch = git_diff_args[0]
1503 else:
1504 if self.GetBranch() is None:
1505 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1506
1507 # Default to diffing against common ancestor of upstream branch
1508 base_branch = self.GetCommonAncestorWithUpstream()
1509 git_diff_args = [base_branch, 'HEAD']
1510
1511 # Make sure authenticated to codereview before running potentially expensive
1512 # hooks. It is a fast, best efforts check. Codereview still can reject the
1513 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001514 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001515
1516 # Apply watchlists on upload.
1517 change = self.GetChange(base_branch, None)
1518 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1519 files = [f.LocalPath() for f in change.AffectedFiles()]
1520 if not options.bypass_watchlists:
1521 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1522
1523 if not options.bypass_hooks:
1524 if options.reviewers or options.tbr_owners:
1525 # Set the reviewer list now so that presubmit checks can access it.
1526 change_description = ChangeDescription(change.FullDescriptionText())
1527 change_description.update_reviewers(options.reviewers,
1528 options.tbr_owners,
1529 change)
1530 change.SetDescriptionText(change_description.description)
1531 hook_results = self.RunHook(committing=False,
1532 may_prompt=not options.force,
1533 verbose=options.verbose,
1534 change=change)
1535 if not hook_results.should_continue():
1536 return 1
1537 if not options.reviewers and hook_results.reviewers:
1538 options.reviewers = hook_results.reviewers.split(',')
1539
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001540 # TODO(tandrii): Checking local patchset against remote patchset is only
1541 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1542 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001543 latest_patchset = self.GetMostRecentPatchset()
1544 local_patchset = self.GetPatchset()
1545 if (latest_patchset and local_patchset and
1546 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001547 print('The last upload made from this repository was patchset #%d but '
1548 'the most recent patchset on the server is #%d.'
1549 % (local_patchset, latest_patchset))
1550 print('Uploading will still work, but if you\'ve uploaded to this '
1551 'issue from another machine or branch the patch you\'re '
1552 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001553 ask_for_data('About to upload; enter to confirm.')
1554
1555 print_stats(options.similarity, options.find_copies, git_diff_args)
1556 ret = self.CMDUploadChange(options, git_diff_args, change)
1557 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001558 if options.use_commit_queue:
1559 self.SetCQState(_CQState.COMMIT)
1560 elif options.cq_dry_run:
1561 self.SetCQState(_CQState.DRY_RUN)
1562
tandrii5d48c322016-08-18 16:19:37 -07001563 _git_set_branch_config_value('last-upload-hash',
1564 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001565 # Run post upload hooks, if specified.
1566 if settings.GetRunPostUploadHook():
1567 presubmit_support.DoPostUploadExecuter(
1568 change,
1569 self,
1570 settings.GetRoot(),
1571 options.verbose,
1572 sys.stdout)
1573
1574 # Upload all dependencies if specified.
1575 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001576 print()
1577 print('--dependencies has been specified.')
1578 print('All dependent local branches will be re-uploaded.')
1579 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001580 # Remove the dependencies flag from args so that we do not end up in a
1581 # loop.
1582 orig_args.remove('--dependencies')
1583 ret = upload_branch_deps(self, orig_args)
1584 return ret
1585
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001586 def SetCQState(self, new_state):
1587 """Update the CQ state for latest patchset.
1588
1589 Issue must have been already uploaded and known.
1590 """
1591 assert new_state in _CQState.ALL_STATES
1592 assert self.GetIssue()
1593 return self._codereview_impl.SetCQState(new_state)
1594
qyearsley1fdfcb62016-10-24 13:22:03 -07001595 def TriggerDryRun(self):
1596 """Triggers a dry run and prints a warning on failure."""
1597 # TODO(qyearsley): Either re-use this method in CMDset_commit
1598 # and CMDupload, or change CMDtry to trigger dry runs with
1599 # just SetCQState, and catch keyboard interrupt and other
1600 # errors in that method.
1601 try:
1602 self.SetCQState(_CQState.DRY_RUN)
1603 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1604 return 0
1605 except KeyboardInterrupt:
1606 raise
1607 except:
1608 print('WARNING: failed to trigger CQ Dry Run.\n'
1609 'Either:\n'
1610 ' * your project has no CQ\n'
1611 ' * you don\'t have permission to trigger Dry Run\n'
1612 ' * bug in this code (see stack trace below).\n'
1613 'Consider specifying which bots to trigger manually '
1614 'or asking your project owners for permissions '
1615 'or contacting Chrome Infrastructure team at '
1616 'https://www.chromium.org/infra\n\n')
1617 # Still raise exception so that stack trace is printed.
1618 raise
1619
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001620 # Forward methods to codereview specific implementation.
1621
1622 def CloseIssue(self):
1623 return self._codereview_impl.CloseIssue()
1624
1625 def GetStatus(self):
1626 return self._codereview_impl.GetStatus()
1627
1628 def GetCodereviewServer(self):
1629 return self._codereview_impl.GetCodereviewServer()
1630
tandriide281ae2016-10-12 06:02:30 -07001631 def GetIssueOwner(self):
1632 """Get owner from codereview, which may differ from this checkout."""
1633 return self._codereview_impl.GetIssueOwner()
1634
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001635 def GetApprovingReviewers(self):
1636 return self._codereview_impl.GetApprovingReviewers()
1637
1638 def GetMostRecentPatchset(self):
1639 return self._codereview_impl.GetMostRecentPatchset()
1640
tandriide281ae2016-10-12 06:02:30 -07001641 def CannotTriggerTryJobReason(self):
1642 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1643 return self._codereview_impl.CannotTriggerTryJobReason()
1644
tandrii8c5a3532016-11-04 07:52:02 -07001645 def GetTryjobProperties(self, patchset=None):
1646 """Returns dictionary of properties to launch tryjob."""
1647 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1648
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001649 def __getattr__(self, attr):
1650 # This is because lots of untested code accesses Rietveld-specific stuff
1651 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001652 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001653 # Note that child method defines __getattr__ as well, and forwards it here,
1654 # because _RietveldChangelistImpl is not cleaned up yet, and given
1655 # deprecation of Rietveld, it should probably be just removed.
1656 # Until that time, avoid infinite recursion by bypassing __getattr__
1657 # of implementation class.
1658 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001659
1660
1661class _ChangelistCodereviewBase(object):
1662 """Abstract base class encapsulating codereview specifics of a changelist."""
1663 def __init__(self, changelist):
1664 self._changelist = changelist # instance of Changelist
1665
1666 def __getattr__(self, attr):
1667 # Forward methods to changelist.
1668 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1669 # _RietveldChangelistImpl to avoid this hack?
1670 return getattr(self._changelist, attr)
1671
1672 def GetStatus(self):
1673 """Apply a rough heuristic to give a simple summary of an issue's review
1674 or CQ status, assuming adherence to a common workflow.
1675
1676 Returns None if no issue for this branch, or specific string keywords.
1677 """
1678 raise NotImplementedError()
1679
1680 def GetCodereviewServer(self):
1681 """Returns server URL without end slash, like "https://codereview.com"."""
1682 raise NotImplementedError()
1683
1684 def FetchDescription(self):
1685 """Fetches and returns description from the codereview server."""
1686 raise NotImplementedError()
1687
tandrii5d48c322016-08-18 16:19:37 -07001688 @classmethod
1689 def IssueConfigKey(cls):
1690 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001691 raise NotImplementedError()
1692
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001693 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001694 def PatchsetConfigKey(cls):
1695 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001696 raise NotImplementedError()
1697
tandrii5d48c322016-08-18 16:19:37 -07001698 @classmethod
1699 def CodereviewServerConfigKey(cls):
1700 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001701 raise NotImplementedError()
1702
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001703 def _PostUnsetIssueProperties(self):
1704 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001705 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001706
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001707 def GetRieveldObjForPresubmit(self):
1708 # This is an unfortunate Rietveld-embeddedness in presubmit.
1709 # For non-Rietveld codereviews, this probably should return a dummy object.
1710 raise NotImplementedError()
1711
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001712 def GetGerritObjForPresubmit(self):
1713 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1714 return None
1715
dsansomee2d6fd92016-09-08 00:10:47 -07001716 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001717 """Update the description on codereview site."""
1718 raise NotImplementedError()
1719
1720 def CloseIssue(self):
1721 """Closes the issue."""
1722 raise NotImplementedError()
1723
1724 def GetApprovingReviewers(self):
1725 """Returns a list of reviewers approving the change.
1726
1727 Note: not necessarily committers.
1728 """
1729 raise NotImplementedError()
1730
1731 def GetMostRecentPatchset(self):
1732 """Returns the most recent patchset number from the codereview site."""
1733 raise NotImplementedError()
1734
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001735 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1736 directory):
1737 """Fetches and applies the issue.
1738
1739 Arguments:
1740 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1741 reject: if True, reject the failed patch instead of switching to 3-way
1742 merge. Rietveld only.
1743 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1744 only.
1745 directory: switch to directory before applying the patch. Rietveld only.
1746 """
1747 raise NotImplementedError()
1748
1749 @staticmethod
1750 def ParseIssueURL(parsed_url):
1751 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1752 failed."""
1753 raise NotImplementedError()
1754
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001755 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001756 """Best effort check that user is authenticated with codereview server.
1757
1758 Arguments:
1759 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001760 refresh: whether to attempt to refresh credentials. Ignored if not
1761 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001762 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001763 raise NotImplementedError()
1764
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001765 def CMDUploadChange(self, options, args, change):
1766 """Uploads a change to codereview."""
1767 raise NotImplementedError()
1768
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001769 def SetCQState(self, new_state):
1770 """Update the CQ state for latest patchset.
1771
1772 Issue must have been already uploaded and known.
1773 """
1774 raise NotImplementedError()
1775
tandriie113dfd2016-10-11 10:20:12 -07001776 def CannotTriggerTryJobReason(self):
1777 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1778 raise NotImplementedError()
1779
tandriide281ae2016-10-12 06:02:30 -07001780 def GetIssueOwner(self):
1781 raise NotImplementedError()
1782
tandrii8c5a3532016-11-04 07:52:02 -07001783 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001784 raise NotImplementedError()
1785
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001786
1787class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1788 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1789 super(_RietveldChangelistImpl, self).__init__(changelist)
1790 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001791 if not rietveld_server:
1792 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001793
1794 self._rietveld_server = rietveld_server
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001795 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001796 self._props = None
1797 self._rpc_server = None
1798
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001799 def GetCodereviewServer(self):
1800 if not self._rietveld_server:
1801 # If we're on a branch then get the server potentially associated
1802 # with that branch.
1803 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001804 self._rietveld_server = gclient_utils.UpgradeToHttps(
1805 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001806 if not self._rietveld_server:
1807 self._rietveld_server = settings.GetDefaultServerUrl()
1808 return self._rietveld_server
1809
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001810 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001811 """Best effort check that user is authenticated with Rietveld server."""
1812 if self._auth_config.use_oauth2:
1813 authenticator = auth.get_authenticator_for_host(
1814 self.GetCodereviewServer(), self._auth_config)
1815 if not authenticator.has_cached_credentials():
1816 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001817 if refresh:
1818 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001819
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001820 def FetchDescription(self):
1821 issue = self.GetIssue()
1822 assert issue
1823 try:
1824 return self.RpcServer().get_description(issue).strip()
1825 except urllib2.HTTPError as e:
1826 if e.code == 404:
1827 DieWithError(
1828 ('\nWhile fetching the description for issue %d, received a '
1829 '404 (not found)\n'
1830 'error. It is likely that you deleted this '
1831 'issue on the server. If this is the\n'
1832 'case, please run\n\n'
1833 ' git cl issue 0\n\n'
1834 'to clear the association with the deleted issue. Then run '
1835 'this command again.') % issue)
1836 else:
1837 DieWithError(
1838 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1839 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001840 print('Warning: Failed to retrieve CL description due to network '
1841 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842 return ''
1843
1844 def GetMostRecentPatchset(self):
1845 return self.GetIssueProperties()['patchsets'][-1]
1846
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001847 def GetIssueProperties(self):
1848 if self._props is None:
1849 issue = self.GetIssue()
1850 if not issue:
1851 self._props = {}
1852 else:
1853 self._props = self.RpcServer().get_issue_properties(issue, True)
1854 return self._props
1855
tandriie113dfd2016-10-11 10:20:12 -07001856 def CannotTriggerTryJobReason(self):
1857 props = self.GetIssueProperties()
1858 if not props:
1859 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1860 if props.get('closed'):
1861 return 'CL %s is closed' % self.GetIssue()
1862 if props.get('private'):
1863 return 'CL %s is private' % self.GetIssue()
1864 return None
1865
tandrii8c5a3532016-11-04 07:52:02 -07001866 def GetTryjobProperties(self, patchset=None):
1867 """Returns dictionary of properties to launch tryjob."""
1868 project = (self.GetIssueProperties() or {}).get('project')
1869 return {
1870 'issue': self.GetIssue(),
1871 'patch_project': project,
1872 'patch_storage': 'rietveld',
1873 'patchset': patchset or self.GetPatchset(),
1874 'rietveld': self.GetCodereviewServer(),
1875 }
1876
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001877 def GetApprovingReviewers(self):
1878 return get_approving_reviewers(self.GetIssueProperties())
1879
tandriide281ae2016-10-12 06:02:30 -07001880 def GetIssueOwner(self):
1881 return (self.GetIssueProperties() or {}).get('owner_email')
1882
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001883 def AddComment(self, message):
1884 return self.RpcServer().add_comment(self.GetIssue(), message)
1885
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001886 def GetStatus(self):
1887 """Apply a rough heuristic to give a simple summary of an issue's review
1888 or CQ status, assuming adherence to a common workflow.
1889
1890 Returns None if no issue for this branch, or one of the following keywords:
1891 * 'error' - error from review tool (including deleted issues)
1892 * 'unsent' - not sent for review
1893 * 'waiting' - waiting for review
1894 * 'reply' - waiting for owner to reply to review
1895 * 'lgtm' - LGTM from at least one approved reviewer
1896 * 'commit' - in the commit queue
1897 * 'closed' - closed
1898 """
1899 if not self.GetIssue():
1900 return None
1901
1902 try:
1903 props = self.GetIssueProperties()
1904 except urllib2.HTTPError:
1905 return 'error'
1906
1907 if props.get('closed'):
1908 # Issue is closed.
1909 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001910 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001911 # Issue is in the commit queue.
1912 return 'commit'
1913
1914 try:
1915 reviewers = self.GetApprovingReviewers()
1916 except urllib2.HTTPError:
1917 return 'error'
1918
1919 if reviewers:
1920 # Was LGTM'ed.
1921 return 'lgtm'
1922
1923 messages = props.get('messages') or []
1924
tandrii9d2c7a32016-06-22 03:42:45 -07001925 # Skip CQ messages that don't require owner's action.
1926 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1927 if 'Dry run:' in messages[-1]['text']:
1928 messages.pop()
1929 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1930 # This message always follows prior messages from CQ,
1931 # so skip this too.
1932 messages.pop()
1933 else:
1934 # This is probably a CQ messages warranting user attention.
1935 break
1936
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001937 if not messages:
1938 # No message was sent.
1939 return 'unsent'
1940 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001941 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001942 return 'reply'
1943 return 'waiting'
1944
dsansomee2d6fd92016-09-08 00:10:47 -07001945 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001946 return self.RpcServer().update_description(
1947 self.GetIssue(), self.description)
1948
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001949 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001950 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001952 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001953 return self.SetFlags({flag: value})
1954
1955 def SetFlags(self, flags):
1956 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001957 """
phajdan.jr68598232016-08-10 03:28:28 -07001958 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001959 try:
tandrii4b233bd2016-07-06 03:50:29 -07001960 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001961 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001962 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001963 if e.code == 404:
1964 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1965 if e.code == 403:
1966 DieWithError(
1967 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001968 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001969 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001971 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001972 """Returns an upload.RpcServer() to access this review's rietveld instance.
1973 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001974 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001975 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001976 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001977 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001978 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001980 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001981 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001982 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001983
tandrii5d48c322016-08-18 16:19:37 -07001984 @classmethod
1985 def PatchsetConfigKey(cls):
1986 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987
tandrii5d48c322016-08-18 16:19:37 -07001988 @classmethod
1989 def CodereviewServerConfigKey(cls):
1990 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001991
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001992 def GetRieveldObjForPresubmit(self):
1993 return self.RpcServer()
1994
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001995 def SetCQState(self, new_state):
1996 props = self.GetIssueProperties()
1997 if props.get('private'):
1998 DieWithError('Cannot set-commit on private issue')
1999
2000 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002001 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002002 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002003 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002004 else:
tandrii4b233bd2016-07-06 03:50:29 -07002005 assert new_state == _CQState.DRY_RUN
2006 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002007
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002008 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2009 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002010 # PatchIssue should never be called with a dirty tree. It is up to the
2011 # caller to check this, but just in case we assert here since the
2012 # consequences of the caller not checking this could be dire.
2013 assert(not git_common.is_dirty_git_tree('apply'))
2014 assert(parsed_issue_arg.valid)
2015 self._changelist.issue = parsed_issue_arg.issue
2016 if parsed_issue_arg.hostname:
2017 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2018
skobes6468b902016-10-24 08:45:10 -07002019 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2020 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2021 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002022 try:
skobes6468b902016-10-24 08:45:10 -07002023 scm_obj.apply_patch(patchset_object)
2024 except Exception as e:
2025 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002026 return 1
2027
2028 # If we had an issue, commit the current state and register the issue.
2029 if not nocommit:
2030 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2031 'patch from issue %(i)s at patchset '
2032 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2033 % {'i': self.GetIssue(), 'p': patchset})])
2034 self.SetIssue(self.GetIssue())
2035 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002036 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002037 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002038 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002039 return 0
2040
2041 @staticmethod
2042 def ParseIssueURL(parsed_url):
2043 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2044 return None
wychen3c1c1722016-08-04 11:46:36 -07002045 # Rietveld patch: https://domain/<number>/#ps<patchset>
2046 match = re.match(r'/(\d+)/$', parsed_url.path)
2047 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2048 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002049 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002050 issue=int(match.group(1)),
2051 patchset=int(match2.group(1)),
2052 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002053 # Typical url: https://domain/<issue_number>[/[other]]
2054 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2055 if match:
skobes6468b902016-10-24 08:45:10 -07002056 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002057 issue=int(match.group(1)),
2058 hostname=parsed_url.netloc)
2059 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2060 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2061 if match:
skobes6468b902016-10-24 08:45:10 -07002062 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002063 issue=int(match.group(1)),
2064 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002065 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002066 return None
2067
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002068 def CMDUploadChange(self, options, args, change):
2069 """Upload the patch to Rietveld."""
2070 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2071 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002072 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2073 if options.emulate_svn_auto_props:
2074 upload_args.append('--emulate_svn_auto_props')
2075
2076 change_desc = None
2077
2078 if options.email is not None:
2079 upload_args.extend(['--email', options.email])
2080
2081 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002082 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002083 upload_args.extend(['--title', options.title])
2084 if options.message:
2085 upload_args.extend(['--message', options.message])
2086 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002087 print('This branch is associated with issue %s. '
2088 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002089 else:
nodirca166002016-06-27 10:59:51 -07002090 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002091 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002092 if options.message:
2093 message = options.message
2094 else:
2095 message = CreateDescriptionFromLog(args)
2096 if options.title:
2097 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002098 change_desc = ChangeDescription(message)
2099 if options.reviewers or options.tbr_owners:
2100 change_desc.update_reviewers(options.reviewers,
2101 options.tbr_owners,
2102 change)
2103 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002104 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002105
2106 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002107 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002108 return 1
2109
2110 upload_args.extend(['--message', change_desc.description])
2111 if change_desc.get_reviewers():
2112 upload_args.append('--reviewers=%s' % ','.join(
2113 change_desc.get_reviewers()))
2114 if options.send_mail:
2115 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002116 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002117 upload_args.append('--send_mail')
2118
2119 # We check this before applying rietveld.private assuming that in
2120 # rietveld.cc only addresses which we can send private CLs to are listed
2121 # if rietveld.private is set, and so we should ignore rietveld.cc only
2122 # when --private is specified explicitly on the command line.
2123 if options.private:
2124 logging.warn('rietveld.cc is ignored since private flag is specified. '
2125 'You need to review and add them manually if necessary.')
2126 cc = self.GetCCListWithoutDefault()
2127 else:
2128 cc = self.GetCCList()
2129 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002130 if change_desc.get_cced():
2131 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002132 if cc:
2133 upload_args.extend(['--cc', cc])
2134
2135 if options.private or settings.GetDefaultPrivateFlag() == "True":
2136 upload_args.append('--private')
2137
2138 upload_args.extend(['--git_similarity', str(options.similarity)])
2139 if not options.find_copies:
2140 upload_args.extend(['--git_no_find_copies'])
2141
2142 # Include the upstream repo's URL in the change -- this is useful for
2143 # projects that have their source spread across multiple repos.
2144 remote_url = self.GetGitBaseUrlFromConfig()
2145 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002146 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2147 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2148 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002149 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002150 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002151 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002152 if target_ref:
2153 upload_args.extend(['--target_ref', target_ref])
2154
2155 # Look for dependent patchsets. See crbug.com/480453 for more details.
2156 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2157 upstream_branch = ShortBranchName(upstream_branch)
2158 if remote is '.':
2159 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002160 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002161 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002162 print()
2163 print('Skipping dependency patchset upload because git config '
2164 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2165 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002166 else:
2167 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002168 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002169 auth_config=auth_config)
2170 branch_cl_issue_url = branch_cl.GetIssueURL()
2171 branch_cl_issue = branch_cl.GetIssue()
2172 branch_cl_patchset = branch_cl.GetPatchset()
2173 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2174 upload_args.extend(
2175 ['--depends_on_patchset', '%s:%s' % (
2176 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002177 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002178 '\n'
2179 'The current branch (%s) is tracking a local branch (%s) with '
2180 'an associated CL.\n'
2181 'Adding %s/#ps%s as a dependency patchset.\n'
2182 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2183 branch_cl_patchset))
2184
2185 project = settings.GetProject()
2186 if project:
2187 upload_args.extend(['--project', project])
2188
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002189 try:
2190 upload_args = ['upload'] + upload_args + args
2191 logging.info('upload.RealMain(%s)', upload_args)
2192 issue, patchset = upload.RealMain(upload_args)
2193 issue = int(issue)
2194 patchset = int(patchset)
2195 except KeyboardInterrupt:
2196 sys.exit(1)
2197 except:
2198 # If we got an exception after the user typed a description for their
2199 # change, back up the description before re-raising.
2200 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002201 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002202 raise
2203
2204 if not self.GetIssue():
2205 self.SetIssue(issue)
2206 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002207 return 0
2208
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002209
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002210class _GerritChangelistImpl(_ChangelistCodereviewBase):
2211 def __init__(self, changelist, auth_config=None):
2212 # auth_config is Rietveld thing, kept here to preserve interface only.
2213 super(_GerritChangelistImpl, self).__init__(changelist)
2214 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002215 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002216 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002217 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002218 # Map from change number (issue) to its detail cache.
2219 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002220
2221 def _GetGerritHost(self):
2222 # Lazy load of configs.
2223 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002224 if self._gerrit_host and '.' not in self._gerrit_host:
2225 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2226 # This happens for internal stuff http://crbug.com/614312.
2227 parsed = urlparse.urlparse(self.GetRemoteUrl())
2228 if parsed.scheme == 'sso':
2229 print('WARNING: using non https URLs for remote is likely broken\n'
2230 ' Your current remote is: %s' % self.GetRemoteUrl())
2231 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2232 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002233 return self._gerrit_host
2234
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002235 def _GetGitHost(self):
2236 """Returns git host to be used when uploading change to Gerrit."""
2237 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2238
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002239 def GetCodereviewServer(self):
2240 if not self._gerrit_server:
2241 # If we're on a branch then get the server potentially associated
2242 # with that branch.
2243 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002244 self._gerrit_server = self._GitGetBranchConfigValue(
2245 self.CodereviewServerConfigKey())
2246 if self._gerrit_server:
2247 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002248 if not self._gerrit_server:
2249 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2250 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002251 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002252 parts[0] = parts[0] + '-review'
2253 self._gerrit_host = '.'.join(parts)
2254 self._gerrit_server = 'https://%s' % self._gerrit_host
2255 return self._gerrit_server
2256
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002257 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002258 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002259 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002260
tandrii5d48c322016-08-18 16:19:37 -07002261 @classmethod
2262 def PatchsetConfigKey(cls):
2263 return 'gerritpatchset'
2264
2265 @classmethod
2266 def CodereviewServerConfigKey(cls):
2267 return 'gerritserver'
2268
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002269 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002270 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002271 if settings.GetGerritSkipEnsureAuthenticated():
2272 # For projects with unusual authentication schemes.
2273 # See http://crbug.com/603378.
2274 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002275 # Lazy-loader to identify Gerrit and Git hosts.
2276 if gerrit_util.GceAuthenticator.is_gce():
2277 return
2278 self.GetCodereviewServer()
2279 git_host = self._GetGitHost()
2280 assert self._gerrit_server and self._gerrit_host
2281 cookie_auth = gerrit_util.CookiesAuthenticator()
2282
2283 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2284 git_auth = cookie_auth.get_auth_header(git_host)
2285 if gerrit_auth and git_auth:
2286 if gerrit_auth == git_auth:
2287 return
2288 print((
2289 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2290 ' Check your %s or %s file for credentials of hosts:\n'
2291 ' %s\n'
2292 ' %s\n'
2293 ' %s') %
2294 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2295 git_host, self._gerrit_host,
2296 cookie_auth.get_new_password_message(git_host)))
2297 if not force:
2298 ask_for_data('If you know what you are doing, press Enter to continue, '
2299 'Ctrl+C to abort.')
2300 return
2301 else:
2302 missing = (
2303 [] if gerrit_auth else [self._gerrit_host] +
2304 [] if git_auth else [git_host])
2305 DieWithError('Credentials for the following hosts are required:\n'
2306 ' %s\n'
2307 'These are read from %s (or legacy %s)\n'
2308 '%s' % (
2309 '\n '.join(missing),
2310 cookie_auth.get_gitcookies_path(),
2311 cookie_auth.get_netrc_path(),
2312 cookie_auth.get_new_password_message(git_host)))
2313
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002314 def _PostUnsetIssueProperties(self):
2315 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002316 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002317
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002318 def GetRieveldObjForPresubmit(self):
2319 class ThisIsNotRietveldIssue(object):
2320 def __nonzero__(self):
2321 # This is a hack to make presubmit_support think that rietveld is not
2322 # defined, yet still ensure that calls directly result in a decent
2323 # exception message below.
2324 return False
2325
2326 def __getattr__(self, attr):
2327 print(
2328 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2329 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2330 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2331 'or use Rietveld for codereview.\n'
2332 'See also http://crbug.com/579160.' % attr)
2333 raise NotImplementedError()
2334 return ThisIsNotRietveldIssue()
2335
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002336 def GetGerritObjForPresubmit(self):
2337 return presubmit_support.GerritAccessor(self._GetGerritHost())
2338
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002339 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002340 """Apply a rough heuristic to give a simple summary of an issue's review
2341 or CQ status, assuming adherence to a common workflow.
2342
2343 Returns None if no issue for this branch, or one of the following keywords:
2344 * 'error' - error from review tool (including deleted issues)
2345 * 'unsent' - no reviewers added
2346 * 'waiting' - waiting for review
2347 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002348 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002349 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002350 * 'commit' - in the commit queue
2351 * 'closed' - abandoned
2352 """
2353 if not self.GetIssue():
2354 return None
2355
2356 try:
2357 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002358 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002359 return 'error'
2360
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002361 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002362 return 'closed'
2363
2364 cq_label = data['labels'].get('Commit-Queue', {})
2365 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002366 votes = cq_label.get('all', [])
2367 highest_vote = 0
2368 for v in votes:
2369 highest_vote = max(highest_vote, v.get('value', 0))
2370 vote_value = str(highest_vote)
2371 if vote_value != '0':
2372 # Add a '+' if the value is not 0 to match the values in the label.
2373 # The cq_label does not have negatives.
2374 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002375 vote_text = cq_label.get('values', {}).get(vote_value, '')
2376 if vote_text.lower() == 'commit':
2377 return 'commit'
2378
2379 lgtm_label = data['labels'].get('Code-Review', {})
2380 if lgtm_label:
2381 if 'rejected' in lgtm_label:
2382 return 'not lgtm'
2383 if 'approved' in lgtm_label:
2384 return 'lgtm'
2385
2386 if not data.get('reviewers', {}).get('REVIEWER', []):
2387 return 'unsent'
2388
2389 messages = data.get('messages', [])
2390 if messages:
2391 owner = data['owner'].get('_account_id')
2392 last_message_author = messages[-1].get('author', {}).get('_account_id')
2393 if owner != last_message_author:
2394 # Some reply from non-owner.
2395 return 'reply'
2396
2397 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002398
2399 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002400 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002401 return data['revisions'][data['current_revision']]['_number']
2402
2403 def FetchDescription(self):
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002404 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002405 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002406 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002407
dsansomee2d6fd92016-09-08 00:10:47 -07002408 def UpdateDescriptionRemote(self, description, force=False):
2409 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2410 if not force:
2411 ask_for_data(
2412 'The description cannot be modified while the issue has a pending '
2413 'unpublished edit. Either publish the edit in the Gerrit web UI '
2414 'or delete it.\n\n'
2415 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2416
2417 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2418 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002419 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002420 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002421
2422 def CloseIssue(self):
2423 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2424
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002425 def GetApprovingReviewers(self):
2426 """Returns a list of reviewers approving the change.
2427
2428 Note: not necessarily committers.
2429 """
2430 raise NotImplementedError()
2431
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002432 def SubmitIssue(self, wait_for_merge=True):
2433 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2434 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002435
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002436 def _GetChangeDetail(self, options=None, issue=None,
2437 no_cache=False):
2438 """Returns details of the issue by querying Gerrit and caching results.
2439
2440 If fresh data is needed, set no_cache=True which will clear cache and
2441 thus new data will be fetched from Gerrit.
2442 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002443 options = options or []
2444 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002445 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002446
2447 # Normalize issue and options for consistent keys in cache.
2448 issue = str(issue)
2449 options = [o.upper() for o in options]
2450
2451 # Check in cache first unless no_cache is True.
2452 if no_cache:
2453 self._detail_cache.pop(issue, None)
2454 else:
2455 options_set = frozenset(options)
2456 for cached_options_set, data in self._detail_cache.get(issue, []):
2457 # Assumption: data fetched before with extra options is suitable
2458 # for return for a smaller set of options.
2459 # For example, if we cached data for
2460 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2461 # and request is for options=[CURRENT_REVISION],
2462 # THEN we can return prior cached data.
2463 if options_set.issubset(cached_options_set):
2464 return data
2465
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002466 try:
2467 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2468 options, ignore_404=False)
2469 except gerrit_util.GerritError as e:
2470 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002471 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002472 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002473
2474 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002475 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002476
agable32978d92016-11-01 12:55:02 -07002477 def _GetChangeCommit(self, issue=None):
2478 issue = issue or self.GetIssue()
2479 assert issue, 'issue is required to query Gerrit'
2480 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2481 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002482 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002483 return data
2484
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002485 def CMDLand(self, force, bypass_hooks, verbose):
2486 if git_common.is_dirty_git_tree('land'):
2487 return 1
tandriid60367b2016-06-22 05:25:12 -07002488 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2489 if u'Commit-Queue' in detail.get('labels', {}):
2490 if not force:
2491 ask_for_data('\nIt seems this repository has a Commit Queue, '
2492 'which can test and land changes for you. '
2493 'Are you sure you wish to bypass it?\n'
2494 'Press Enter to continue, Ctrl+C to abort.')
2495
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002496 differs = True
tandriic4344b52016-08-29 06:04:54 -07002497 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002498 # Note: git diff outputs nothing if there is no diff.
2499 if not last_upload or RunGit(['diff', last_upload]).strip():
2500 print('WARNING: some changes from local branch haven\'t been uploaded')
2501 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002502 if detail['current_revision'] == last_upload:
2503 differs = False
2504 else:
2505 print('WARNING: local branch contents differ from latest uploaded '
2506 'patchset')
2507 if differs:
2508 if not force:
2509 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002510 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2511 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002512 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2513 elif not bypass_hooks:
2514 hook_results = self.RunHook(
2515 committing=True,
2516 may_prompt=not force,
2517 verbose=verbose,
2518 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2519 if not hook_results.should_continue():
2520 return 1
2521
2522 self.SubmitIssue(wait_for_merge=True)
2523 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002524 links = self._GetChangeCommit().get('web_links', [])
2525 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002526 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002527 print('Landed as %s' % link.get('url'))
2528 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002529 return 0
2530
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002531 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2532 directory):
2533 assert not reject
2534 assert not nocommit
2535 assert not directory
2536 assert parsed_issue_arg.valid
2537
2538 self._changelist.issue = parsed_issue_arg.issue
2539
2540 if parsed_issue_arg.hostname:
2541 self._gerrit_host = parsed_issue_arg.hostname
2542 self._gerrit_server = 'https://%s' % self._gerrit_host
2543
tandriic2405f52016-10-10 08:13:15 -07002544 try:
2545 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002546 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002547 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002548
2549 if not parsed_issue_arg.patchset:
2550 # Use current revision by default.
2551 revision_info = detail['revisions'][detail['current_revision']]
2552 patchset = int(revision_info['_number'])
2553 else:
2554 patchset = parsed_issue_arg.patchset
2555 for revision_info in detail['revisions'].itervalues():
2556 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2557 break
2558 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002559 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002560 (parsed_issue_arg.patchset, self.GetIssue()))
2561
2562 fetch_info = revision_info['fetch']['http']
2563 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2564 RunGit(['cherry-pick', 'FETCH_HEAD'])
2565 self.SetIssue(self.GetIssue())
2566 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002567 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002568 (self.GetIssue(), self.GetPatchset()))
2569 return 0
2570
2571 @staticmethod
2572 def ParseIssueURL(parsed_url):
2573 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2574 return None
2575 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2576 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2577 # Short urls like https://domain/<issue_number> can be used, but don't allow
2578 # specifying the patchset (you'd 404), but we allow that here.
2579 if parsed_url.path == '/':
2580 part = parsed_url.fragment
2581 else:
2582 part = parsed_url.path
2583 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2584 if match:
2585 return _ParsedIssueNumberArgument(
2586 issue=int(match.group(2)),
2587 patchset=int(match.group(4)) if match.group(4) else None,
2588 hostname=parsed_url.netloc)
2589 return None
2590
tandrii16e0b4e2016-06-07 10:34:28 -07002591 def _GerritCommitMsgHookCheck(self, offer_removal):
2592 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2593 if not os.path.exists(hook):
2594 return
2595 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2596 # custom developer made one.
2597 data = gclient_utils.FileRead(hook)
2598 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2599 return
2600 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002601 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002602 'and may interfere with it in subtle ways.\n'
2603 'We recommend you remove the commit-msg hook.')
2604 if offer_removal:
2605 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2606 if reply.lower().startswith('y'):
2607 gclient_utils.rm_file_or_tree(hook)
2608 print('Gerrit commit-msg hook removed.')
2609 else:
2610 print('OK, will keep Gerrit commit-msg hook in place.')
2611
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002612 def CMDUploadChange(self, options, args, change):
2613 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002614 if options.squash and options.no_squash:
2615 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002616
2617 if not options.squash and not options.no_squash:
2618 # Load default for user, repo, squash=true, in this order.
2619 options.squash = settings.GetSquashGerritUploads()
2620 elif options.no_squash:
2621 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002622
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002623 # We assume the remote called "origin" is the one we want.
2624 # It is probably not worthwhile to support different workflows.
2625 gerrit_remote = 'origin'
2626
2627 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002628 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002629
Aaron Gableb56ad332017-01-06 15:24:31 -08002630 # This may be None; default fallback value is determined in logic below.
2631 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002632 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002633
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002634 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002635 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002636 if self.GetIssue():
2637 # Try to get the message from a previous upload.
2638 message = self.GetDescription()
2639 if not message:
2640 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002641 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002642 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002643 if not title:
2644 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2645 title = ask_for_data(
2646 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002647 if title == default_title:
2648 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002649 change_id = self._GetChangeDetail()['change_id']
2650 while True:
2651 footer_change_ids = git_footers.get_footer_change_id(message)
2652 if footer_change_ids == [change_id]:
2653 break
2654 if not footer_change_ids:
2655 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002656 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002657 continue
2658 # There is already a valid footer but with different or several ids.
2659 # Doing this automatically is non-trivial as we don't want to lose
2660 # existing other footers, yet we want to append just 1 desired
2661 # Change-Id. Thus, just create a new footer, but let user verify the
2662 # new description.
2663 message = '%s\n\nChange-Id: %s' % (message, change_id)
2664 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002665 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002666 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002667 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002668 'Please, check the proposed correction to the description, '
2669 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2670 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2671 change_id))
2672 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2673 if not options.force:
2674 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002675 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002676 message = change_desc.description
2677 if not message:
2678 DieWithError("Description is empty. Aborting...")
2679 # Continue the while loop.
2680 # Sanity check of this code - we should end up with proper message
2681 # footer.
2682 assert [change_id] == git_footers.get_footer_change_id(message)
2683 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002684 else: # if not self.GetIssue()
2685 if options.message:
2686 message = options.message
2687 else:
2688 message = CreateDescriptionFromLog(args)
2689 if options.title:
2690 message = options.title + '\n\n' + message
2691 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002692 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002693 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002694 # On first upload, patchset title is always this string, while
2695 # --title flag gets converted to first line of message.
2696 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002697 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002698 if not change_desc.description:
2699 DieWithError("Description is empty. Aborting...")
2700 message = change_desc.description
2701 change_ids = git_footers.get_footer_change_id(message)
2702 if len(change_ids) > 1:
2703 DieWithError('too many Change-Id footers, at most 1 allowed.')
2704 if not change_ids:
2705 # Generate the Change-Id automatically.
2706 message = git_footers.add_footer_change_id(
2707 message, GenerateGerritChangeId(message))
2708 change_desc.set_description(message)
2709 change_ids = git_footers.get_footer_change_id(message)
2710 assert len(change_ids) == 1
2711 change_id = change_ids[0]
2712
2713 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2714 if remote is '.':
2715 # If our upstream branch is local, we base our squashed commit on its
2716 # squashed version.
2717 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2718 # Check the squashed hash of the parent.
2719 parent = RunGit(['config',
2720 'branch.%s.gerritsquashhash' % upstream_branch_name],
2721 error_ok=True).strip()
2722 # Verify that the upstream branch has been uploaded too, otherwise
2723 # Gerrit will create additional CLs when uploading.
2724 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2725 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002726 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002727 '\nUpload upstream branch %s first.\n'
2728 'It is likely that this branch has been rebased since its last '
2729 'upload, so you just need to upload it again.\n'
2730 '(If you uploaded it with --no-squash, then branch dependencies '
2731 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002732 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002733 else:
2734 parent = self.GetCommonAncestorWithUpstream()
2735
2736 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2737 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2738 '-m', message]).strip()
2739 else:
2740 change_desc = ChangeDescription(
2741 options.message or CreateDescriptionFromLog(args))
2742 if not change_desc.description:
2743 DieWithError("Description is empty. Aborting...")
2744
2745 if not git_footers.get_footer_change_id(change_desc.description):
2746 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002747 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2748 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002749 ref_to_push = 'HEAD'
2750 parent = '%s/%s' % (gerrit_remote, branch)
2751 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2752
2753 assert change_desc
2754 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2755 ref_to_push)]).splitlines()
2756 if len(commits) > 1:
2757 print('WARNING: This will upload %d commits. Run the following command '
2758 'to see which commits will be uploaded: ' % len(commits))
2759 print('git log %s..%s' % (parent, ref_to_push))
2760 print('You can also use `git squash-branch` to squash these into a '
2761 'single commit.')
2762 ask_for_data('About to upload; enter to confirm.')
2763
2764 if options.reviewers or options.tbr_owners:
2765 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2766 change)
2767
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002768 # Extra options that can be specified at push time. Doc:
2769 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2770 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002771 if change_desc.get_reviewers(tbr_only=True):
2772 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2773 refspec_opts.append('l=Code-Review+1')
2774
Aaron Gable9b713dd2016-12-14 16:04:21 -08002775 if title:
2776 if not re.match(r'^[\w ]+$', title):
2777 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002778 if not automatic_title:
2779 print('WARNING: Patchset title may only contain alphanumeric chars '
2780 'and spaces. Cleaned up title:\n%s' % title)
2781 if not options.force:
2782 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002783 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2784 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002785 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002786
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002787 if options.send_mail:
2788 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002789 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002790 refspec_opts.append('notify=ALL')
2791 else:
2792 refspec_opts.append('notify=NONE')
2793
tandrii99a72f22016-08-17 14:33:24 -07002794 reviewers = change_desc.get_reviewers()
2795 if reviewers:
2796 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002797
agablec6787972016-09-09 16:13:34 -07002798 if options.private:
2799 refspec_opts.append('draft')
2800
rmistry9eadede2016-09-19 11:22:43 -07002801 if options.topic:
2802 # Documentation on Gerrit topics is here:
2803 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2804 refspec_opts.append('topic=%s' % options.topic)
2805
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002806 refspec_suffix = ''
2807 if refspec_opts:
2808 refspec_suffix = '%' + ','.join(refspec_opts)
2809 assert ' ' not in refspec_suffix, (
2810 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002811 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002812
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002813 try:
2814 push_stdout = gclient_utils.CheckCallAndFilter(
2815 ['git', 'push', gerrit_remote, refspec],
2816 print_stdout=True,
2817 # Flush after every line: useful for seeing progress when running as
2818 # recipe.
2819 filter_fn=lambda _: sys.stdout.flush())
2820 except subprocess2.CalledProcessError:
2821 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002822 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002823
2824 if options.squash:
2825 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2826 change_numbers = [m.group(1)
2827 for m in map(regex.match, push_stdout.splitlines())
2828 if m]
2829 if len(change_numbers) != 1:
2830 DieWithError(
2831 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002832 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002833 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002834 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002835
2836 # Add cc's from the CC_LIST and --cc flag (if any).
2837 cc = self.GetCCList().split(',')
2838 if options.cc:
2839 cc.extend(options.cc)
2840 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002841 if change_desc.get_cced():
2842 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002843 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002844 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002845 self._GetGerritHost(), self.GetIssue(), cc,
2846 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002847 return 0
2848
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002849 def _AddChangeIdToCommitMessage(self, options, args):
2850 """Re-commits using the current message, assumes the commit hook is in
2851 place.
2852 """
2853 log_desc = options.message or CreateDescriptionFromLog(args)
2854 git_command = ['commit', '--amend', '-m', log_desc]
2855 RunGit(git_command)
2856 new_log_desc = CreateDescriptionFromLog(args)
2857 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002858 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002859 return new_log_desc
2860 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002861 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002862
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002863 def SetCQState(self, new_state):
2864 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002865 vote_map = {
2866 _CQState.NONE: 0,
2867 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002868 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002869 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002870 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2871 if new_state == _CQState.DRY_RUN:
2872 # Don't spam everybody reviewer/owner.
2873 kwargs['notify'] = 'NONE'
2874 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002875
tandriie113dfd2016-10-11 10:20:12 -07002876 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002877 try:
2878 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002879 except GerritChangeNotExists:
2880 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002881
2882 if data['status'] in ('ABANDONED', 'MERGED'):
2883 return 'CL %s is closed' % self.GetIssue()
2884
2885 def GetTryjobProperties(self, patchset=None):
2886 """Returns dictionary of properties to launch tryjob."""
2887 data = self._GetChangeDetail(['ALL_REVISIONS'])
2888 patchset = int(patchset or self.GetPatchset())
2889 assert patchset
2890 revision_data = None # Pylint wants it to be defined.
2891 for revision_data in data['revisions'].itervalues():
2892 if int(revision_data['_number']) == patchset:
2893 break
2894 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002895 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002896 (patchset, self.GetIssue()))
2897 return {
2898 'patch_issue': self.GetIssue(),
2899 'patch_set': patchset or self.GetPatchset(),
2900 'patch_project': data['project'],
2901 'patch_storage': 'gerrit',
2902 'patch_ref': revision_data['fetch']['http']['ref'],
2903 'patch_repository_url': revision_data['fetch']['http']['url'],
2904 'patch_gerrit_url': self.GetCodereviewServer(),
2905 }
tandriie113dfd2016-10-11 10:20:12 -07002906
tandriide281ae2016-10-12 06:02:30 -07002907 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002908 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002909
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002910
2911_CODEREVIEW_IMPLEMENTATIONS = {
2912 'rietveld': _RietveldChangelistImpl,
2913 'gerrit': _GerritChangelistImpl,
2914}
2915
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002916
iannuccie53c9352016-08-17 14:40:40 -07002917def _add_codereview_issue_select_options(parser, extra=""):
2918 _add_codereview_select_options(parser)
2919
2920 text = ('Operate on this issue number instead of the current branch\'s '
2921 'implicit issue.')
2922 if extra:
2923 text += ' '+extra
2924 parser.add_option('-i', '--issue', type=int, help=text)
2925
2926
2927def _process_codereview_issue_select_options(parser, options):
2928 _process_codereview_select_options(parser, options)
2929 if options.issue is not None and not options.forced_codereview:
2930 parser.error('--issue must be specified with either --rietveld or --gerrit')
2931
2932
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002933def _add_codereview_select_options(parser):
2934 """Appends --gerrit and --rietveld options to force specific codereview."""
2935 parser.codereview_group = optparse.OptionGroup(
2936 parser, 'EXPERIMENTAL! Codereview override options')
2937 parser.add_option_group(parser.codereview_group)
2938 parser.codereview_group.add_option(
2939 '--gerrit', action='store_true',
2940 help='Force the use of Gerrit for codereview')
2941 parser.codereview_group.add_option(
2942 '--rietveld', action='store_true',
2943 help='Force the use of Rietveld for codereview')
2944
2945
2946def _process_codereview_select_options(parser, options):
2947 if options.gerrit and options.rietveld:
2948 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2949 options.forced_codereview = None
2950 if options.gerrit:
2951 options.forced_codereview = 'gerrit'
2952 elif options.rietveld:
2953 options.forced_codereview = 'rietveld'
2954
2955
tandriif9aefb72016-07-01 09:06:51 -07002956def _get_bug_line_values(default_project, bugs):
2957 """Given default_project and comma separated list of bugs, yields bug line
2958 values.
2959
2960 Each bug can be either:
2961 * a number, which is combined with default_project
2962 * string, which is left as is.
2963
2964 This function may produce more than one line, because bugdroid expects one
2965 project per line.
2966
2967 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2968 ['v8:123', 'chromium:789']
2969 """
2970 default_bugs = []
2971 others = []
2972 for bug in bugs.split(','):
2973 bug = bug.strip()
2974 if bug:
2975 try:
2976 default_bugs.append(int(bug))
2977 except ValueError:
2978 others.append(bug)
2979
2980 if default_bugs:
2981 default_bugs = ','.join(map(str, default_bugs))
2982 if default_project:
2983 yield '%s:%s' % (default_project, default_bugs)
2984 else:
2985 yield default_bugs
2986 for other in sorted(others):
2987 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2988 yield other
2989
2990
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002991class ChangeDescription(object):
2992 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002993 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002994 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002995 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002996 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002997
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002998 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002999 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003000
agable@chromium.org42c20792013-09-12 17:34:49 +00003001 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003002 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003003 return '\n'.join(self._description_lines)
3004
3005 def set_description(self, desc):
3006 if isinstance(desc, basestring):
3007 lines = desc.splitlines()
3008 else:
3009 lines = [line.rstrip() for line in desc]
3010 while lines and not lines[0]:
3011 lines.pop(0)
3012 while lines and not lines[-1]:
3013 lines.pop(-1)
3014 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003015
piman@chromium.org336f9122014-09-04 02:16:55 +00003016 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003017 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003018 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003019 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003020 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003021 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003022
agable@chromium.org42c20792013-09-12 17:34:49 +00003023 # Get the set of R= and TBR= lines and remove them from the desciption.
3024 regexp = re.compile(self.R_LINE)
3025 matches = [regexp.match(line) for line in self._description_lines]
3026 new_desc = [l for i, l in enumerate(self._description_lines)
3027 if not matches[i]]
3028 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003029
agable@chromium.org42c20792013-09-12 17:34:49 +00003030 # Construct new unified R= and TBR= lines.
3031 r_names = []
3032 tbr_names = []
3033 for match in matches:
3034 if not match:
3035 continue
3036 people = cleanup_list([match.group(2).strip()])
3037 if match.group(1) == 'TBR':
3038 tbr_names.extend(people)
3039 else:
3040 r_names.extend(people)
3041 for name in r_names:
3042 if name not in reviewers:
3043 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003044 if add_owners_tbr:
3045 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003046 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003047 all_reviewers = set(tbr_names + reviewers)
3048 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3049 all_reviewers)
3050 tbr_names.extend(owners_db.reviewers_for(missing_files,
3051 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003052 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3053 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3054
3055 # Put the new lines in the description where the old first R= line was.
3056 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3057 if 0 <= line_loc < len(self._description_lines):
3058 if new_tbr_line:
3059 self._description_lines.insert(line_loc, new_tbr_line)
3060 if new_r_line:
3061 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003062 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003063 if new_r_line:
3064 self.append_footer(new_r_line)
3065 if new_tbr_line:
3066 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003067
tandriif9aefb72016-07-01 09:06:51 -07003068 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003069 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003070 self.set_description([
3071 '# Enter a description of the change.',
3072 '# This will be displayed on the codereview site.',
3073 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003074 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003075 '--------------------',
3076 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003077
agable@chromium.org42c20792013-09-12 17:34:49 +00003078 regexp = re.compile(self.BUG_LINE)
3079 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003080 prefix = settings.GetBugPrefix()
3081 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3082 for value in values:
3083 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3084 self.append_footer('BUG=%s' % value)
3085
agable@chromium.org42c20792013-09-12 17:34:49 +00003086 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003087 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003088 if not content:
3089 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003090 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003091
3092 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003093 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3094 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003095 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003096 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003097
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003098 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003099 """Adds a footer line to the description.
3100
3101 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3102 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3103 that Gerrit footers are always at the end.
3104 """
3105 parsed_footer_line = git_footers.parse_footer(line)
3106 if parsed_footer_line:
3107 # Line is a gerrit footer in the form: Footer-Key: any value.
3108 # Thus, must be appended observing Gerrit footer rules.
3109 self.set_description(
3110 git_footers.add_footer(self.description,
3111 key=parsed_footer_line[0],
3112 value=parsed_footer_line[1]))
3113 return
3114
3115 if not self._description_lines:
3116 self._description_lines.append(line)
3117 return
3118
3119 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3120 if gerrit_footers:
3121 # git_footers.split_footers ensures that there is an empty line before
3122 # actual (gerrit) footers, if any. We have to keep it that way.
3123 assert top_lines and top_lines[-1] == ''
3124 top_lines, separator = top_lines[:-1], top_lines[-1:]
3125 else:
3126 separator = [] # No need for separator if there are no gerrit_footers.
3127
3128 prev_line = top_lines[-1] if top_lines else ''
3129 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3130 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3131 top_lines.append('')
3132 top_lines.append(line)
3133 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003134
tandrii99a72f22016-08-17 14:33:24 -07003135 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003136 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003137 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003138 reviewers = [match.group(2).strip()
3139 for match in matches
3140 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003141 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003142
bradnelsond975b302016-10-23 12:20:23 -07003143 def get_cced(self):
3144 """Retrieves the list of reviewers."""
3145 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3146 cced = [match.group(2).strip() for match in matches if match]
3147 return cleanup_list(cced)
3148
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003149 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3150 """Updates this commit description given the parent.
3151
3152 This is essentially what Gnumbd used to do.
3153 Consult https://goo.gl/WMmpDe for more details.
3154 """
3155 assert parent_msg # No, orphan branch creation isn't supported.
3156 assert parent_hash
3157 assert dest_ref
3158 parent_footer_map = git_footers.parse_footers(parent_msg)
3159 # This will also happily parse svn-position, which GnumbD is no longer
3160 # supporting. While we'd generate correct footers, the verifier plugin
3161 # installed in Gerrit will block such commit (ie git push below will fail).
3162 parent_position = git_footers.get_position(parent_footer_map)
3163
3164 # Cherry-picks may have last line obscuring their prior footers,
3165 # from git_footers perspective. This is also what Gnumbd did.
3166 cp_line = None
3167 if (self._description_lines and
3168 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3169 cp_line = self._description_lines.pop()
3170
3171 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3172
3173 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3174 # user interference with actual footers we'd insert below.
3175 for i, (k, v) in enumerate(parsed_footers):
3176 if k.startswith('Cr-'):
3177 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3178
3179 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003180 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003181 if parent_position[0] == dest_ref:
3182 # Same branch as parent.
3183 number = int(parent_position[1]) + 1
3184 else:
3185 number = 1 # New branch, and extra lineage.
3186 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3187 int(parent_position[1])))
3188
3189 parsed_footers.append(('Cr-Commit-Position',
3190 '%s@{#%d}' % (dest_ref, number)))
3191 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3192
3193 self._description_lines = top_lines
3194 if cp_line:
3195 self._description_lines.append(cp_line)
3196 if self._description_lines[-1] != '':
3197 self._description_lines.append('') # Ensure footer separator.
3198 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3199
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003200
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003201def get_approving_reviewers(props):
3202 """Retrieves the reviewers that approved a CL from the issue properties with
3203 messages.
3204
3205 Note that the list may contain reviewers that are not committer, thus are not
3206 considered by the CQ.
3207 """
3208 return sorted(
3209 set(
3210 message['sender']
3211 for message in props['messages']
3212 if message['approval'] and message['sender'] in props['reviewers']
3213 )
3214 )
3215
3216
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003217def FindCodereviewSettingsFile(filename='codereview.settings'):
3218 """Finds the given file starting in the cwd and going up.
3219
3220 Only looks up to the top of the repository unless an
3221 'inherit-review-settings-ok' file exists in the root of the repository.
3222 """
3223 inherit_ok_file = 'inherit-review-settings-ok'
3224 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003225 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003226 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3227 root = '/'
3228 while True:
3229 if filename in os.listdir(cwd):
3230 if os.path.isfile(os.path.join(cwd, filename)):
3231 return open(os.path.join(cwd, filename))
3232 if cwd == root:
3233 break
3234 cwd = os.path.dirname(cwd)
3235
3236
3237def LoadCodereviewSettingsFromFile(fileobj):
3238 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003239 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003240
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003241 def SetProperty(name, setting, unset_error_ok=False):
3242 fullname = 'rietveld.' + name
3243 if setting in keyvals:
3244 RunGit(['config', fullname, keyvals[setting]])
3245 else:
3246 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3247
tandrii48df5812016-10-17 03:55:37 -07003248 if not keyvals.get('GERRIT_HOST', False):
3249 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003250 # Only server setting is required. Other settings can be absent.
3251 # In that case, we ignore errors raised during option deletion attempt.
3252 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003253 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003254 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3255 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003256 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003257 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3258 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003259 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003260 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3261 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003263 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003264 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003265
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003266 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003267 RunGit(['config', 'gerrit.squash-uploads',
3268 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003269
tandrii@chromium.org28253532016-04-14 13:46:56 +00003270 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003271 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003272 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003274 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003275 # should be of the form
3276 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3277 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003278 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3279 keyvals['ORIGIN_URL_CONFIG']])
3280
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003281
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003282def urlretrieve(source, destination):
3283 """urllib is broken for SSL connections via a proxy therefore we
3284 can't use urllib.urlretrieve()."""
3285 with open(destination, 'w') as f:
3286 f.write(urllib2.urlopen(source).read())
3287
3288
ukai@chromium.org712d6102013-11-27 00:52:58 +00003289def hasSheBang(fname):
3290 """Checks fname is a #! script."""
3291 with open(fname) as f:
3292 return f.read(2).startswith('#!')
3293
3294
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003295# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3296def DownloadHooks(*args, **kwargs):
3297 pass
3298
3299
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003300def DownloadGerritHook(force):
3301 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003302
3303 Args:
3304 force: True to update hooks. False to install hooks if not present.
3305 """
3306 if not settings.GetIsGerrit():
3307 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003308 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003309 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3310 if not os.access(dst, os.X_OK):
3311 if os.path.exists(dst):
3312 if not force:
3313 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003314 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003315 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003316 if not hasSheBang(dst):
3317 DieWithError('Not a script: %s\n'
3318 'You need to download from\n%s\n'
3319 'into .git/hooks/commit-msg and '
3320 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003321 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3322 except Exception:
3323 if os.path.exists(dst):
3324 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003325 DieWithError('\nFailed to download hooks.\n'
3326 'You need to download from\n%s\n'
3327 'into .git/hooks/commit-msg and '
3328 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003329
3330
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003331def GetRietveldCodereviewSettingsInteractively():
3332 """Prompt the user for settings."""
3333 server = settings.GetDefaultServerUrl(error_ok=True)
3334 prompt = 'Rietveld server (host[:port])'
3335 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3336 newserver = ask_for_data(prompt + ':')
3337 if not server and not newserver:
3338 newserver = DEFAULT_SERVER
3339 if newserver:
3340 newserver = gclient_utils.UpgradeToHttps(newserver)
3341 if newserver != server:
3342 RunGit(['config', 'rietveld.server', newserver])
3343
3344 def SetProperty(initial, caption, name, is_url):
3345 prompt = caption
3346 if initial:
3347 prompt += ' ("x" to clear) [%s]' % initial
3348 new_val = ask_for_data(prompt + ':')
3349 if new_val == 'x':
3350 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3351 elif new_val:
3352 if is_url:
3353 new_val = gclient_utils.UpgradeToHttps(new_val)
3354 if new_val != initial:
3355 RunGit(['config', 'rietveld.' + name, new_val])
3356
3357 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3358 SetProperty(settings.GetDefaultPrivateFlag(),
3359 'Private flag (rietveld only)', 'private', False)
3360 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3361 'tree-status-url', False)
3362 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3363 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3364 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3365 'run-post-upload-hook', False)
3366
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003367
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003368@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003369def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003370 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003371
tandrii5d0a0422016-09-14 06:24:35 -07003372 print('WARNING: git cl config works for Rietveld only')
3373 # TODO(tandrii): remove this once we switch to Gerrit.
3374 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003375 parser.add_option('--activate-update', action='store_true',
3376 help='activate auto-updating [rietveld] section in '
3377 '.git/config')
3378 parser.add_option('--deactivate-update', action='store_true',
3379 help='deactivate auto-updating [rietveld] section in '
3380 '.git/config')
3381 options, args = parser.parse_args(args)
3382
3383 if options.deactivate_update:
3384 RunGit(['config', 'rietveld.autoupdate', 'false'])
3385 return
3386
3387 if options.activate_update:
3388 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3389 return
3390
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003391 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003392 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003393 return 0
3394
3395 url = args[0]
3396 if not url.endswith('codereview.settings'):
3397 url = os.path.join(url, 'codereview.settings')
3398
3399 # Load code review settings and download hooks (if available).
3400 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3401 return 0
3402
3403
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003404def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003405 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003406 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3407 branch = ShortBranchName(branchref)
3408 _, args = parser.parse_args(args)
3409 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003410 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003411 return RunGit(['config', 'branch.%s.base-url' % branch],
3412 error_ok=False).strip()
3413 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003414 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003415 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3416 error_ok=False).strip()
3417
3418
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003419def color_for_status(status):
3420 """Maps a Changelist status to color, for CMDstatus and other tools."""
3421 return {
3422 'unsent': Fore.RED,
3423 'waiting': Fore.BLUE,
3424 'reply': Fore.YELLOW,
3425 'lgtm': Fore.GREEN,
3426 'commit': Fore.MAGENTA,
3427 'closed': Fore.CYAN,
3428 'error': Fore.WHITE,
3429 }.get(status, Fore.WHITE)
3430
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003431
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003432def get_cl_statuses(changes, fine_grained, max_processes=None):
3433 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003434
3435 If fine_grained is true, this will fetch CL statuses from the server.
3436 Otherwise, simply indicate if there's a matching url for the given branches.
3437
3438 If max_processes is specified, it is used as the maximum number of processes
3439 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3440 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003441
3442 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003443 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003444 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003445 upload.verbosity = 0
3446
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003447 if not changes:
3448 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003449
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003450 if not fine_grained:
3451 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003452 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003453 for cl in changes:
3454 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003455 return
3456
3457 # First, sort out authentication issues.
3458 logging.debug('ensuring credentials exist')
3459 for cl in changes:
3460 cl.EnsureAuthenticated(force=False, refresh=True)
3461
3462 def fetch(cl):
3463 try:
3464 return (cl, cl.GetStatus())
3465 except:
3466 # See http://crbug.com/629863.
3467 logging.exception('failed to fetch status for %s:', cl)
3468 raise
3469
3470 threads_count = len(changes)
3471 if max_processes:
3472 threads_count = max(1, min(threads_count, max_processes))
3473 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3474
3475 pool = ThreadPool(threads_count)
3476 fetched_cls = set()
3477 try:
3478 it = pool.imap_unordered(fetch, changes).__iter__()
3479 while True:
3480 try:
3481 cl, status = it.next(timeout=5)
3482 except multiprocessing.TimeoutError:
3483 break
3484 fetched_cls.add(cl)
3485 yield cl, status
3486 finally:
3487 pool.close()
3488
3489 # Add any branches that failed to fetch.
3490 for cl in set(changes) - fetched_cls:
3491 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003492
rmistry@google.com2dd99862015-06-22 12:22:18 +00003493
3494def upload_branch_deps(cl, args):
3495 """Uploads CLs of local branches that are dependents of the current branch.
3496
3497 If the local branch dependency tree looks like:
3498 test1 -> test2.1 -> test3.1
3499 -> test3.2
3500 -> test2.2 -> test3.3
3501
3502 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3503 run on the dependent branches in this order:
3504 test2.1, test3.1, test3.2, test2.2, test3.3
3505
3506 Note: This function does not rebase your local dependent branches. Use it when
3507 you make a change to the parent branch that will not conflict with its
3508 dependent branches, and you would like their dependencies updated in
3509 Rietveld.
3510 """
3511 if git_common.is_dirty_git_tree('upload-branch-deps'):
3512 return 1
3513
3514 root_branch = cl.GetBranch()
3515 if root_branch is None:
3516 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3517 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003518 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003519 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3520 'patchset dependencies without an uploaded CL.')
3521
3522 branches = RunGit(['for-each-ref',
3523 '--format=%(refname:short) %(upstream:short)',
3524 'refs/heads'])
3525 if not branches:
3526 print('No local branches found.')
3527 return 0
3528
3529 # Create a dictionary of all local branches to the branches that are dependent
3530 # on it.
3531 tracked_to_dependents = collections.defaultdict(list)
3532 for b in branches.splitlines():
3533 tokens = b.split()
3534 if len(tokens) == 2:
3535 branch_name, tracked = tokens
3536 tracked_to_dependents[tracked].append(branch_name)
3537
vapiera7fbd5a2016-06-16 09:17:49 -07003538 print()
3539 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003540 dependents = []
3541 def traverse_dependents_preorder(branch, padding=''):
3542 dependents_to_process = tracked_to_dependents.get(branch, [])
3543 padding += ' '
3544 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003545 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003546 dependents.append(dependent)
3547 traverse_dependents_preorder(dependent, padding)
3548 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003549 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003550
3551 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003552 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003553 return 0
3554
vapiera7fbd5a2016-06-16 09:17:49 -07003555 print('This command will checkout all dependent branches and run '
3556 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003557 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3558
andybons@chromium.org962f9462016-02-03 20:00:42 +00003559 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003560 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003561 args.extend(['-t', 'Updated patchset dependency'])
3562
rmistry@google.com2dd99862015-06-22 12:22:18 +00003563 # Record all dependents that failed to upload.
3564 failures = {}
3565 # Go through all dependents, checkout the branch and upload.
3566 try:
3567 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003568 print()
3569 print('--------------------------------------')
3570 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003571 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003572 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003573 try:
3574 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003575 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003576 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003577 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003578 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003579 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003580 finally:
3581 # Swap back to the original root branch.
3582 RunGit(['checkout', '-q', root_branch])
3583
vapiera7fbd5a2016-06-16 09:17:49 -07003584 print()
3585 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003586 for dependent_branch in dependents:
3587 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003588 print(' %s : %s' % (dependent_branch, upload_status))
3589 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003590
3591 return 0
3592
3593
kmarshall3bff56b2016-06-06 18:31:47 -07003594def CMDarchive(parser, args):
3595 """Archives and deletes branches associated with closed changelists."""
3596 parser.add_option(
3597 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003598 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003599 parser.add_option(
3600 '-f', '--force', action='store_true',
3601 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003602 parser.add_option(
3603 '-d', '--dry-run', action='store_true',
3604 help='Skip the branch tagging and removal steps.')
3605 parser.add_option(
3606 '-t', '--notags', action='store_true',
3607 help='Do not tag archived branches. '
3608 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003609
3610 auth.add_auth_options(parser)
3611 options, args = parser.parse_args(args)
3612 if args:
3613 parser.error('Unsupported args: %s' % ' '.join(args))
3614 auth_config = auth.extract_auth_config_from_options(options)
3615
3616 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3617 if not branches:
3618 return 0
3619
vapiera7fbd5a2016-06-16 09:17:49 -07003620 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003621 changes = [Changelist(branchref=b, auth_config=auth_config)
3622 for b in branches.splitlines()]
3623 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3624 statuses = get_cl_statuses(changes,
3625 fine_grained=True,
3626 max_processes=options.maxjobs)
3627 proposal = [(cl.GetBranch(),
3628 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3629 for cl, status in statuses
3630 if status == 'closed']
3631 proposal.sort()
3632
3633 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003634 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003635 return 0
3636
3637 current_branch = GetCurrentBranch()
3638
vapiera7fbd5a2016-06-16 09:17:49 -07003639 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003640 if options.notags:
3641 for next_item in proposal:
3642 print(' ' + next_item[0])
3643 else:
3644 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3645 for next_item in proposal:
3646 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003647
kmarshall9249e012016-08-23 12:02:16 -07003648 # Quit now on precondition failure or if instructed by the user, either
3649 # via an interactive prompt or by command line flags.
3650 if options.dry_run:
3651 print('\nNo changes were made (dry run).\n')
3652 return 0
3653 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003654 print('You are currently on a branch \'%s\' which is associated with a '
3655 'closed codereview issue, so archive cannot proceed. Please '
3656 'checkout another branch and run this command again.' %
3657 current_branch)
3658 return 1
kmarshall9249e012016-08-23 12:02:16 -07003659 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003660 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3661 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003662 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003663 return 1
3664
3665 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003666 if not options.notags:
3667 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003668 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003669
vapiera7fbd5a2016-06-16 09:17:49 -07003670 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003671
3672 return 0
3673
3674
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003675def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003676 """Show status of changelists.
3677
3678 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003679 - Red not sent for review or broken
3680 - Blue waiting for review
3681 - Yellow waiting for you to reply to review
3682 - Green LGTM'ed
3683 - Magenta in the commit queue
3684 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003685
3686 Also see 'git cl comments'.
3687 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003688 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003689 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003690 parser.add_option('-f', '--fast', action='store_true',
3691 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003692 parser.add_option(
3693 '-j', '--maxjobs', action='store', type=int,
3694 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003695
3696 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003697 _add_codereview_issue_select_options(
3698 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003699 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003700 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003701 if args:
3702 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003703 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003704
iannuccie53c9352016-08-17 14:40:40 -07003705 if options.issue is not None and not options.field:
3706 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003707
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003708 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003709 cl = Changelist(auth_config=auth_config, issue=options.issue,
3710 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003711 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003712 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003713 elif options.field == 'id':
3714 issueid = cl.GetIssue()
3715 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003716 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003717 elif options.field == 'patch':
3718 patchset = cl.GetPatchset()
3719 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003720 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003721 elif options.field == 'status':
3722 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003723 elif options.field == 'url':
3724 url = cl.GetIssueURL()
3725 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003726 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003727 return 0
3728
3729 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3730 if not branches:
3731 print('No local branch found.')
3732 return 0
3733
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003734 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003735 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003736 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003737 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003738 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003739 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003740 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003741
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003742 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003743 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3744 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3745 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003746 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003747 c, status = output.next()
3748 branch_statuses[c.GetBranch()] = status
3749 status = branch_statuses.pop(branch)
3750 url = cl.GetIssueURL()
3751 if url and (not status or status == 'error'):
3752 # The issue probably doesn't exist anymore.
3753 url += ' (broken)'
3754
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003755 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003756 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003757 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003758 color = ''
3759 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003760 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003761 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003762 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003763 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003764
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003765
3766 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003767 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003768 print('Current branch: %s' % branch)
3769 for cl in changes:
3770 if cl.GetBranch() == branch:
3771 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003772 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003773 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003774 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003775 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003776 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003777 print('Issue description:')
3778 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003779 return 0
3780
3781
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003782def colorize_CMDstatus_doc():
3783 """To be called once in main() to add colors to git cl status help."""
3784 colors = [i for i in dir(Fore) if i[0].isupper()]
3785
3786 def colorize_line(line):
3787 for color in colors:
3788 if color in line.upper():
3789 # Extract whitespaces first and the leading '-'.
3790 indent = len(line) - len(line.lstrip(' ')) + 1
3791 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3792 return line
3793
3794 lines = CMDstatus.__doc__.splitlines()
3795 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3796
3797
phajdan.jre328cf92016-08-22 04:12:17 -07003798def write_json(path, contents):
3799 with open(path, 'w') as f:
3800 json.dump(contents, f)
3801
3802
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003803@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003804def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003805 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806
3807 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003808 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003809 parser.add_option('-r', '--reverse', action='store_true',
3810 help='Lookup the branch(es) for the specified issues. If '
3811 'no issues are specified, all branches with mapped '
3812 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003813 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003814 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003815 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003816 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003817
dnj@chromium.org406c4402015-03-03 17:22:28 +00003818 if options.reverse:
3819 branches = RunGit(['for-each-ref', 'refs/heads',
3820 '--format=%(refname:short)']).splitlines()
3821
3822 # Reverse issue lookup.
3823 issue_branch_map = {}
3824 for branch in branches:
3825 cl = Changelist(branchref=branch)
3826 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3827 if not args:
3828 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003829 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003830 for issue in args:
3831 if not issue:
3832 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003833 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003834 print('Branch for issue number %s: %s' % (
3835 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003836 if options.json:
3837 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003838 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003839 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003840 if len(args) > 0:
3841 try:
3842 issue = int(args[0])
3843 except ValueError:
3844 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003845 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003846 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003847 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003848 if options.json:
3849 write_json(options.json, {
3850 'issue': cl.GetIssue(),
3851 'issue_url': cl.GetIssueURL(),
3852 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003853 return 0
3854
3855
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003856def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003857 """Shows or posts review comments for any changelist."""
3858 parser.add_option('-a', '--add-comment', dest='comment',
3859 help='comment to add to an issue')
3860 parser.add_option('-i', dest='issue',
3861 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003862 parser.add_option('-j', '--json-file',
3863 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003864 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003865 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003866 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003867
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003868 issue = None
3869 if options.issue:
3870 try:
3871 issue = int(options.issue)
3872 except ValueError:
3873 DieWithError('A review issue id is expected to be a number')
3874
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003875 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003876
3877 if options.comment:
3878 cl.AddComment(options.comment)
3879 return 0
3880
3881 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003882 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003883 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003884 summary.append({
3885 'date': message['date'],
3886 'lgtm': False,
3887 'message': message['text'],
3888 'not_lgtm': False,
3889 'sender': message['sender'],
3890 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003891 if message['disapproval']:
3892 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003893 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003894 elif message['approval']:
3895 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003896 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003897 elif message['sender'] == data['owner_email']:
3898 color = Fore.MAGENTA
3899 else:
3900 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003901 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003902 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003903 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003904 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003905 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003906 if options.json_file:
3907 with open(options.json_file, 'wb') as f:
3908 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003909 return 0
3910
3911
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003912@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003913def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003914 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003915 parser.add_option('-d', '--display', action='store_true',
3916 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003917 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003918 help='New description to set for this issue (- for stdin, '
3919 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003920 parser.add_option('-f', '--force', action='store_true',
3921 help='Delete any unpublished Gerrit edits for this issue '
3922 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003923
3924 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003925 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003926 options, args = parser.parse_args(args)
3927 _process_codereview_select_options(parser, options)
3928
3929 target_issue = None
3930 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003931 target_issue = ParseIssueNumberArgument(args[0])
3932 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003933 parser.print_help()
3934 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003935
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003936 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003937
martiniss6eda05f2016-06-30 10:18:35 -07003938 kwargs = {
3939 'auth_config': auth_config,
3940 'codereview': options.forced_codereview,
3941 }
3942 if target_issue:
3943 kwargs['issue'] = target_issue.issue
3944 if options.forced_codereview == 'rietveld':
3945 kwargs['rietveld_server'] = target_issue.hostname
3946
3947 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003948
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003949 if not cl.GetIssue():
3950 DieWithError('This branch has no associated changelist.')
3951 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003952
smut@google.com34fb6b12015-07-13 20:03:26 +00003953 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003954 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003955 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003956
3957 if options.new_description:
3958 text = options.new_description
3959 if text == '-':
3960 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003961 elif text == '+':
3962 base_branch = cl.GetCommonAncestorWithUpstream()
3963 change = cl.GetChange(base_branch, None, local_description=True)
3964 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003965
3966 description.set_description(text)
3967 else:
3968 description.prompt()
3969
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003970 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003971 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003972 return 0
3973
3974
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003975def CreateDescriptionFromLog(args):
3976 """Pulls out the commit log to use as a base for the CL description."""
3977 log_args = []
3978 if len(args) == 1 and not args[0].endswith('.'):
3979 log_args = [args[0] + '..']
3980 elif len(args) == 1 and args[0].endswith('...'):
3981 log_args = [args[0][:-1]]
3982 elif len(args) == 2:
3983 log_args = [args[0] + '..' + args[1]]
3984 else:
3985 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003986 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003987
3988
thestig@chromium.org44202a22014-03-11 19:22:18 +00003989def CMDlint(parser, args):
3990 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003991 parser.add_option('--filter', action='append', metavar='-x,+y',
3992 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003993 auth.add_auth_options(parser)
3994 options, args = parser.parse_args(args)
3995 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003996
3997 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003998 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00003999 try:
4000 import cpplint
4001 import cpplint_chromium
4002 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004003 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004004 return 1
4005
4006 # Change the current working directory before calling lint so that it
4007 # shows the correct base.
4008 previous_cwd = os.getcwd()
4009 os.chdir(settings.GetRoot())
4010 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004011 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004012 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4013 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004014 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004015 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004016 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004017
4018 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004019 command = args + files
4020 if options.filter:
4021 command = ['--filter=' + ','.join(options.filter)] + command
4022 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004023
4024 white_regex = re.compile(settings.GetLintRegex())
4025 black_regex = re.compile(settings.GetLintIgnoreRegex())
4026 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4027 for filename in filenames:
4028 if white_regex.match(filename):
4029 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004030 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004031 else:
4032 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4033 extra_check_functions)
4034 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004035 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004036 finally:
4037 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004038 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004039 if cpplint._cpplint_state.error_count != 0:
4040 return 1
4041 return 0
4042
4043
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004044def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004045 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004046 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004047 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004048 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004049 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004050 auth.add_auth_options(parser)
4051 options, args = parser.parse_args(args)
4052 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004053
sbc@chromium.org71437c02015-04-09 19:29:40 +00004054 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004055 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004056 return 1
4057
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004058 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004059 if args:
4060 base_branch = args[0]
4061 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004062 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004063 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004064
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004065 cl.RunHook(
4066 committing=not options.upload,
4067 may_prompt=False,
4068 verbose=options.verbose,
4069 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004070 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004071
4072
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004073def GenerateGerritChangeId(message):
4074 """Returns Ixxxxxx...xxx change id.
4075
4076 Works the same way as
4077 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4078 but can be called on demand on all platforms.
4079
4080 The basic idea is to generate git hash of a state of the tree, original commit
4081 message, author/committer info and timestamps.
4082 """
4083 lines = []
4084 tree_hash = RunGitSilent(['write-tree'])
4085 lines.append('tree %s' % tree_hash.strip())
4086 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4087 if code == 0:
4088 lines.append('parent %s' % parent.strip())
4089 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4090 lines.append('author %s' % author.strip())
4091 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4092 lines.append('committer %s' % committer.strip())
4093 lines.append('')
4094 # Note: Gerrit's commit-hook actually cleans message of some lines and
4095 # whitespace. This code is not doing this, but it clearly won't decrease
4096 # entropy.
4097 lines.append(message)
4098 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4099 stdin='\n'.join(lines))
4100 return 'I%s' % change_hash.strip()
4101
4102
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004103def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004104 """Computes the remote branch ref to use for the CL.
4105
4106 Args:
4107 remote (str): The git remote for the CL.
4108 remote_branch (str): The git remote branch for the CL.
4109 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004110 """
4111 if not (remote and remote_branch):
4112 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004113
wittman@chromium.org455dc922015-01-26 20:15:50 +00004114 if target_branch:
4115 # Cannonicalize branch references to the equivalent local full symbolic
4116 # refs, which are then translated into the remote full symbolic refs
4117 # below.
4118 if '/' not in target_branch:
4119 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4120 else:
4121 prefix_replacements = (
4122 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4123 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4124 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4125 )
4126 match = None
4127 for regex, replacement in prefix_replacements:
4128 match = re.search(regex, target_branch)
4129 if match:
4130 remote_branch = target_branch.replace(match.group(0), replacement)
4131 break
4132 if not match:
4133 # This is a branch path but not one we recognize; use as-is.
4134 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004135 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4136 # Handle the refs that need to land in different refs.
4137 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004138
wittman@chromium.org455dc922015-01-26 20:15:50 +00004139 # Create the true path to the remote branch.
4140 # Does the following translation:
4141 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4142 # * refs/remotes/origin/master -> refs/heads/master
4143 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4144 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4145 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4146 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4147 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4148 'refs/heads/')
4149 elif remote_branch.startswith('refs/remotes/branch-heads'):
4150 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004151
wittman@chromium.org455dc922015-01-26 20:15:50 +00004152 return remote_branch
4153
4154
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004155def cleanup_list(l):
4156 """Fixes a list so that comma separated items are put as individual items.
4157
4158 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4159 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4160 """
4161 items = sum((i.split(',') for i in l), [])
4162 stripped_items = (i.strip() for i in items)
4163 return sorted(filter(None, stripped_items))
4164
4165
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004166@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004167def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004168 """Uploads the current changelist to codereview.
4169
4170 Can skip dependency patchset uploads for a branch by running:
4171 git config branch.branch_name.skip-deps-uploads True
4172 To unset run:
4173 git config --unset branch.branch_name.skip-deps-uploads
4174 Can also set the above globally by using the --global flag.
4175 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004176 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4177 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004178 parser.add_option('--bypass-watchlists', action='store_true',
4179 dest='bypass_watchlists',
4180 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004181 parser.add_option('-f', action='store_true', dest='force',
4182 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004183 parser.add_option('--message', '-m', dest='message',
4184 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004185 parser.add_option('-b', '--bug',
4186 help='pre-populate the bug number(s) for this issue. '
4187 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004188 parser.add_option('--message-file', dest='message_file',
4189 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004190 parser.add_option('--title', '-t', dest='title',
4191 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004192 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004193 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004194 help='reviewer email addresses')
4195 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004196 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004197 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004198 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004199 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004200 parser.add_option('--emulate_svn_auto_props',
4201 '--emulate-svn-auto-props',
4202 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004203 dest="emulate_svn_auto_props",
4204 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004205 parser.add_option('-c', '--use-commit-queue', action='store_true',
4206 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004207 parser.add_option('--private', action='store_true',
4208 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004209 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004210 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004211 metavar='TARGET',
4212 help='Apply CL to remote ref TARGET. ' +
4213 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004214 parser.add_option('--squash', action='store_true',
4215 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004216 parser.add_option('--no-squash', action='store_true',
4217 help='Don\'t squash multiple commits into one ' +
4218 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004219 parser.add_option('--topic', default=None,
4220 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004221 parser.add_option('--email', default=None,
4222 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004223 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4224 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004225 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4226 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004227 help='Send the patchset to do a CQ dry run right after '
4228 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004229 parser.add_option('--dependencies', action='store_true',
4230 help='Uploads CLs of all the local branches that depend on '
4231 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004232
rmistry@google.com2dd99862015-06-22 12:22:18 +00004233 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004234 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004235 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004236 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004237 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004238 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004239 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004240
sbc@chromium.org71437c02015-04-09 19:29:40 +00004241 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004242 return 1
4243
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004244 options.reviewers = cleanup_list(options.reviewers)
4245 options.cc = cleanup_list(options.cc)
4246
tandriib80458a2016-06-23 12:20:07 -07004247 if options.message_file:
4248 if options.message:
4249 parser.error('only one of --message and --message-file allowed.')
4250 options.message = gclient_utils.FileRead(options.message_file)
4251 options.message_file = None
4252
tandrii4d0545a2016-07-06 03:56:49 -07004253 if options.cq_dry_run and options.use_commit_queue:
4254 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4255
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004256 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4257 settings.GetIsGerrit()
4258
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004259 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004260 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004261
4262
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004263@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004264def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004265 """DEPRECATED: Used to commit the current changelist via git-svn."""
4266 message = ('git-cl no longer supports committing to SVN repositories via '
4267 'git-svn. You probably want to use `git cl land` instead.')
4268 print(message)
4269 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004270
4271
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004272@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004273def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004274 """Commits the current changelist via git.
4275
4276 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4277 upstream and closes the issue automatically and atomically.
4278
4279 Otherwise (in case of Rietveld):
4280 Squashes branch into a single commit.
4281 Updates commit message with metadata (e.g. pointer to review).
4282 Pushes the code upstream.
4283 Updates review and closes.
4284 """
4285 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4286 help='bypass upload presubmit hook')
4287 parser.add_option('-m', dest='message',
4288 help="override review description")
4289 parser.add_option('-f', action='store_true', dest='force',
4290 help="force yes to questions (don't prompt)")
4291 parser.add_option('-c', dest='contributor',
4292 help="external contributor for patch (appended to " +
4293 "description and used as author for git). Should be " +
4294 "formatted as 'First Last <email@example.com>'")
4295 add_git_similarity(parser)
4296 auth.add_auth_options(parser)
4297 (options, args) = parser.parse_args(args)
4298 auth_config = auth.extract_auth_config_from_options(options)
4299
4300 cl = Changelist(auth_config=auth_config)
4301
4302 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4303 if cl.IsGerrit():
4304 if options.message:
4305 # This could be implemented, but it requires sending a new patch to
4306 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4307 # Besides, Gerrit has the ability to change the commit message on submit
4308 # automatically, thus there is no need to support this option (so far?).
4309 parser.error('-m MESSAGE option is not supported for Gerrit.')
4310 if options.contributor:
4311 parser.error(
4312 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4313 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4314 'the contributor\'s "name <email>". If you can\'t upload such a '
4315 'commit for review, contact your repository admin and request'
4316 '"Forge-Author" permission.')
4317 if not cl.GetIssue():
4318 DieWithError('You must upload the change first to Gerrit.\n'
4319 ' If you would rather have `git cl land` upload '
4320 'automatically for you, see http://crbug.com/642759')
4321 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4322 options.verbose)
4323
4324 current = cl.GetBranch()
4325 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4326 if remote == '.':
4327 print()
4328 print('Attempting to push branch %r into another local branch!' % current)
4329 print()
4330 print('Either reparent this branch on top of origin/master:')
4331 print(' git reparent-branch --root')
4332 print()
4333 print('OR run `git rebase-update` if you think the parent branch is ')
4334 print('already committed.')
4335 print()
4336 print(' Current parent: %r' % upstream_branch)
4337 return 1
4338
4339 if not args:
4340 # Default to merging against our best guess of the upstream branch.
4341 args = [cl.GetUpstreamBranch()]
4342
4343 if options.contributor:
4344 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4345 print("Please provide contibutor as 'First Last <email@example.com>'")
4346 return 1
4347
4348 base_branch = args[0]
4349
4350 if git_common.is_dirty_git_tree('land'):
4351 return 1
4352
4353 # This rev-list syntax means "show all commits not in my branch that
4354 # are in base_branch".
4355 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4356 base_branch]).splitlines()
4357 if upstream_commits:
4358 print('Base branch "%s" has %d commits '
4359 'not in this branch.' % (base_branch, len(upstream_commits)))
4360 print('Run "git merge %s" before attempting to land.' % base_branch)
4361 return 1
4362
4363 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4364 if not options.bypass_hooks:
4365 author = None
4366 if options.contributor:
4367 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4368 hook_results = cl.RunHook(
4369 committing=True,
4370 may_prompt=not options.force,
4371 verbose=options.verbose,
4372 change=cl.GetChange(merge_base, author))
4373 if not hook_results.should_continue():
4374 return 1
4375
4376 # Check the tree status if the tree status URL is set.
4377 status = GetTreeStatus()
4378 if 'closed' == status:
4379 print('The tree is closed. Please wait for it to reopen. Use '
4380 '"git cl land --bypass-hooks" to commit on a closed tree.')
4381 return 1
4382 elif 'unknown' == status:
4383 print('Unable to determine tree status. Please verify manually and '
4384 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4385 return 1
4386
4387 change_desc = ChangeDescription(options.message)
4388 if not change_desc.description and cl.GetIssue():
4389 change_desc = ChangeDescription(cl.GetDescription())
4390
4391 if not change_desc.description:
4392 if not cl.GetIssue() and options.bypass_hooks:
4393 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4394 else:
4395 print('No description set.')
4396 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4397 return 1
4398
4399 # Keep a separate copy for the commit message, because the commit message
4400 # contains the link to the Rietveld issue, while the Rietveld message contains
4401 # the commit viewvc url.
4402 if cl.GetIssue():
4403 change_desc.update_reviewers(cl.GetApprovingReviewers())
4404
4405 commit_desc = ChangeDescription(change_desc.description)
4406 if cl.GetIssue():
4407 # Xcode won't linkify this URL unless there is a non-whitespace character
4408 # after it. Add a period on a new line to circumvent this. Also add a space
4409 # before the period to make sure that Gitiles continues to correctly resolve
4410 # the URL.
4411 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4412 if options.contributor:
4413 commit_desc.append_footer('Patch from %s.' % options.contributor)
4414
4415 print('Description:')
4416 print(commit_desc.description)
4417
4418 branches = [merge_base, cl.GetBranchRef()]
4419 if not options.force:
4420 print_stats(options.similarity, options.find_copies, branches)
4421
4422 # We want to squash all this branch's commits into one commit with the proper
4423 # description. We do this by doing a "reset --soft" to the base branch (which
4424 # keeps the working copy the same), then landing that.
4425 MERGE_BRANCH = 'git-cl-commit'
4426 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4427 # Delete the branches if they exist.
4428 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4429 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4430 result = RunGitWithCode(showref_cmd)
4431 if result[0] == 0:
4432 RunGit(['branch', '-D', branch])
4433
4434 # We might be in a directory that's present in this branch but not in the
4435 # trunk. Move up to the top of the tree so that git commands that expect a
4436 # valid CWD won't fail after we check out the merge branch.
4437 rel_base_path = settings.GetRelativeRoot()
4438 if rel_base_path:
4439 os.chdir(rel_base_path)
4440
4441 # Stuff our change into the merge branch.
4442 # We wrap in a try...finally block so if anything goes wrong,
4443 # we clean up the branches.
4444 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004445 revision = None
4446 try:
4447 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4448 RunGit(['reset', '--soft', merge_base])
4449 if options.contributor:
4450 RunGit(
4451 [
4452 'commit', '--author', options.contributor,
4453 '-m', commit_desc.description,
4454 ])
4455 else:
4456 RunGit(['commit', '-m', commit_desc.description])
4457
4458 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4459 mirror = settings.GetGitMirror(remote)
4460 if mirror:
4461 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004462 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004463 else:
4464 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004465 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004466 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4467
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004468 if git_numberer_enabled:
4469 # TODO(tandrii): maybe do autorebase + retry on failure
4470 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004471 logging.debug('Adding git number footers')
4472 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4473 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4474 branch)
4475 # Ensure timestamps are monotonically increasing.
4476 timestamp = max(1 + _get_committer_timestamp(merge_base),
4477 _get_committer_timestamp('HEAD'))
4478 _git_amend_head(commit_desc.description, timestamp)
4479 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004480
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004481 retcode, output = RunGitWithCode(
4482 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004483 if retcode == 0:
4484 revision = RunGit(['rev-parse', 'HEAD']).strip()
4485 logging.debug(output)
4486 except: # pylint: disable=bare-except
4487 if _IS_BEING_TESTED:
4488 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4489 + '-' * 30 + '8<' + '-' * 30)
4490 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4491 raise
4492 finally:
4493 # And then swap back to the original branch and clean up.
4494 RunGit(['checkout', '-q', cl.GetBranch()])
4495 RunGit(['branch', '-D', MERGE_BRANCH])
4496
4497 if not revision:
4498 print('Failed to push. If this persists, please file a bug.')
4499 return 1
4500
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004501 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004502 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004503 if viewvc_url and revision:
4504 change_desc.append_footer(
4505 'Committed: %s%s' % (viewvc_url, revision))
4506 elif revision:
4507 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004508 print('Closing issue '
4509 '(you may be prompted for your codereview password)...')
4510 cl.UpdateDescription(change_desc.description)
4511 cl.CloseIssue()
4512 props = cl.GetIssueProperties()
4513 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004514 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4515 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004516 if options.bypass_hooks:
4517 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4518 else:
4519 comment += ' (presubmit successful).'
4520 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4521
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004522 if os.path.isfile(POSTUPSTREAM_HOOK):
4523 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4524
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004525 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004526
4527
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004528@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004529def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004530 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004531 parser.add_option('-b', dest='newbranch',
4532 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004533 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004534 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004535 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4536 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004537 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004538 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004539 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004540 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004541 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004542 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004543
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004544
4545 group = optparse.OptionGroup(
4546 parser,
4547 'Options for continuing work on the current issue uploaded from a '
4548 'different clone (e.g. different machine). Must be used independently '
4549 'from the other options. No issue number should be specified, and the '
4550 'branch must have an issue number associated with it')
4551 group.add_option('--reapply', action='store_true', dest='reapply',
4552 help='Reset the branch and reapply the issue.\n'
4553 'CAUTION: This will undo any local changes in this '
4554 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004555
4556 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004557 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004558 parser.add_option_group(group)
4559
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004560 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004561 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004562 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004563 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004564 auth_config = auth.extract_auth_config_from_options(options)
4565
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004566
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004567 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004568 if options.newbranch:
4569 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004570 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004571 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004572
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004573 cl = Changelist(auth_config=auth_config,
4574 codereview=options.forced_codereview)
4575 if not cl.GetIssue():
4576 parser.error('current branch must have an associated issue')
4577
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004578 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004579 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004580 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004581
4582 RunGit(['reset', '--hard', upstream])
4583 if options.pull:
4584 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004585
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004586 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4587 options.directory)
4588
4589 if len(args) != 1 or not args[0]:
4590 parser.error('Must specify issue number or url')
4591
4592 # We don't want uncommitted changes mixed up with the patch.
4593 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004594 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004595
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004596 if options.newbranch:
4597 if options.force:
4598 RunGit(['branch', '-D', options.newbranch],
4599 stderr=subprocess2.PIPE, error_ok=True)
4600 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004601 elif not GetCurrentBranch():
4602 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004603
4604 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4605
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004606 if cl.IsGerrit():
4607 if options.reject:
4608 parser.error('--reject is not supported with Gerrit codereview.')
4609 if options.nocommit:
4610 parser.error('--nocommit is not supported with Gerrit codereview.')
4611 if options.directory:
4612 parser.error('--directory is not supported with Gerrit codereview.')
4613
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004614 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004615 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004616
4617
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004618def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004619 """Fetches the tree status and returns either 'open', 'closed',
4620 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004621 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004622 if url:
4623 status = urllib2.urlopen(url).read().lower()
4624 if status.find('closed') != -1 or status == '0':
4625 return 'closed'
4626 elif status.find('open') != -1 or status == '1':
4627 return 'open'
4628 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004629 return 'unset'
4630
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004632def GetTreeStatusReason():
4633 """Fetches the tree status from a json url and returns the message
4634 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004635 url = settings.GetTreeStatusUrl()
4636 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004637 connection = urllib2.urlopen(json_url)
4638 status = json.loads(connection.read())
4639 connection.close()
4640 return status['message']
4641
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004642
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004643def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004644 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004645 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004646 status = GetTreeStatus()
4647 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004648 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004649 return 2
4650
vapiera7fbd5a2016-06-16 09:17:49 -07004651 print('The tree is %s' % status)
4652 print()
4653 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004654 if status != 'open':
4655 return 1
4656 return 0
4657
4658
maruel@chromium.org15192402012-09-06 12:38:29 +00004659def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004660 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004661 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004662 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004663 '-b', '--bot', action='append',
4664 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4665 'times to specify multiple builders. ex: '
4666 '"-b win_rel -b win_layout". See '
4667 'the try server waterfall for the builders name and the tests '
4668 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004669 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004670 '-B', '--bucket', default='',
4671 help=('Buildbucket bucket to send the try requests.'))
4672 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004673 '-m', '--master', default='',
4674 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004675 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004676 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004677 help='Revision to use for the try job; default: the revision will '
4678 'be determined by the try recipe that builder runs, which usually '
4679 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004680 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004681 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004682 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004683 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004684 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004685 '--project',
4686 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004687 'in recipe to determine to which repository or directory to '
4688 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004689 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004690 '-p', '--property', dest='properties', action='append', default=[],
4691 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004692 'key2=value2 etc. The value will be treated as '
4693 'json if decodable, or as string otherwise. '
4694 'NOTE: using this may make your try job not usable for CQ, '
4695 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004696 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004697 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4698 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004699 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004700 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004701 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004702 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004703
machenbach@chromium.org45453142015-09-15 08:45:22 +00004704 # Make sure that all properties are prop=value pairs.
4705 bad_params = [x for x in options.properties if '=' not in x]
4706 if bad_params:
4707 parser.error('Got properties with missing "=": %s' % bad_params)
4708
maruel@chromium.org15192402012-09-06 12:38:29 +00004709 if args:
4710 parser.error('Unknown arguments: %s' % args)
4711
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004712 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004713 if not cl.GetIssue():
4714 parser.error('Need to upload first')
4715
tandriie113dfd2016-10-11 10:20:12 -07004716 error_message = cl.CannotTriggerTryJobReason()
4717 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004718 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004719
borenet6c0efe62016-10-19 08:13:29 -07004720 if options.bucket and options.master:
4721 parser.error('Only one of --bucket and --master may be used.')
4722
qyearsley1fdfcb62016-10-24 13:22:03 -07004723 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004724
qyearsleydd49f942016-10-28 11:57:22 -07004725 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4726 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004727 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004728 if options.verbose:
4729 print('git cl try with no bots now defaults to CQ Dry Run.')
4730 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004731
borenet6c0efe62016-10-19 08:13:29 -07004732 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004733 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004734 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004735 'of bot requires an initial job from a parent (usually a builder). '
4736 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004737 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004738 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004739
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004740 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004741 # TODO(tandrii): Checking local patchset against remote patchset is only
4742 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4743 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004744 print('Warning: Codereview server has newer patchsets (%s) than most '
4745 'recent upload from local checkout (%s). Did a previous upload '
4746 'fail?\n'
4747 'By default, git cl try uses the latest patchset from '
4748 'codereview, continuing to use patchset %s.\n' %
4749 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004750
tandrii568043b2016-10-11 07:49:18 -07004751 try:
borenet6c0efe62016-10-19 08:13:29 -07004752 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4753 patchset)
tandrii568043b2016-10-11 07:49:18 -07004754 except BuildbucketResponseException as ex:
4755 print('ERROR: %s' % ex)
4756 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004757 return 0
4758
4759
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004760def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004761 """Prints info about try jobs associated with current CL."""
4762 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004763 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004764 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004765 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004766 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004767 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004768 '--color', action='store_true', default=setup_color.IS_TTY,
4769 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004770 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004771 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4772 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004773 group.add_option(
4774 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004775 parser.add_option_group(group)
4776 auth.add_auth_options(parser)
4777 options, args = parser.parse_args(args)
4778 if args:
4779 parser.error('Unrecognized args: %s' % ' '.join(args))
4780
4781 auth_config = auth.extract_auth_config_from_options(options)
4782 cl = Changelist(auth_config=auth_config)
4783 if not cl.GetIssue():
4784 parser.error('Need to upload first')
4785
tandrii221ab252016-10-06 08:12:04 -07004786 patchset = options.patchset
4787 if not patchset:
4788 patchset = cl.GetMostRecentPatchset()
4789 if not patchset:
4790 parser.error('Codereview doesn\'t know about issue %s. '
4791 'No access to issue or wrong issue number?\n'
4792 'Either upload first, or pass --patchset explicitely' %
4793 cl.GetIssue())
4794
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004795 # TODO(tandrii): Checking local patchset against remote patchset is only
4796 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4797 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004798 print('Warning: Codereview server has newer patchsets (%s) than most '
4799 'recent upload from local checkout (%s). Did a previous upload '
4800 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004801 'By default, git cl try-results uses the latest patchset from '
4802 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004803 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004804 try:
tandrii221ab252016-10-06 08:12:04 -07004805 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004806 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004807 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004808 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004809 if options.json:
4810 write_try_results_json(options.json, jobs)
4811 else:
4812 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004813 return 0
4814
4815
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004816@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004817def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004818 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004819 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004820 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004821 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004822
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004823 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004824 if args:
4825 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004826 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004827 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004828 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004829 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004830
4831 # Clear configured merge-base, if there is one.
4832 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004833 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004834 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004835 return 0
4836
4837
thestig@chromium.org00858c82013-12-02 23:08:03 +00004838def CMDweb(parser, args):
4839 """Opens the current CL in the web browser."""
4840 _, args = parser.parse_args(args)
4841 if args:
4842 parser.error('Unrecognized args: %s' % ' '.join(args))
4843
4844 issue_url = Changelist().GetIssueURL()
4845 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004846 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004847 return 1
4848
4849 webbrowser.open(issue_url)
4850 return 0
4851
4852
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004853def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004854 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004855 parser.add_option('-d', '--dry-run', action='store_true',
4856 help='trigger in dry run mode')
4857 parser.add_option('-c', '--clear', action='store_true',
4858 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004859 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004860 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004861 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004862 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004863 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004864 if args:
4865 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004866 if options.dry_run and options.clear:
4867 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4868
iannuccie53c9352016-08-17 14:40:40 -07004869 cl = Changelist(auth_config=auth_config, issue=options.issue,
4870 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004871 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004872 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004873 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07004874 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004875 state = _CQState.DRY_RUN
4876 else:
4877 state = _CQState.COMMIT
4878 if not cl.GetIssue():
4879 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004880 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004881 return 0
4882
4883
groby@chromium.org411034a2013-02-26 15:12:01 +00004884def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004885 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004886 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004887 auth.add_auth_options(parser)
4888 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004889 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004890 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004891 if args:
4892 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004893 cl = Changelist(auth_config=auth_config, issue=options.issue,
4894 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004895 # Ensure there actually is an issue to close.
4896 cl.GetDescription()
4897 cl.CloseIssue()
4898 return 0
4899
4900
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004901def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004902 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004903 parser.add_option(
4904 '--stat',
4905 action='store_true',
4906 dest='stat',
4907 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004908 auth.add_auth_options(parser)
4909 options, args = parser.parse_args(args)
4910 auth_config = auth.extract_auth_config_from_options(options)
4911 if args:
4912 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004913
4914 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004915 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004916 # Staged changes would be committed along with the patch from last
4917 # upload, hence counted toward the "last upload" side in the final
4918 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004919 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004920 return 1
4921
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004922 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004923 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004924 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004925 if not issue:
4926 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004927 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004928 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004929
4930 # Create a new branch based on the merge-base
4931 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004932 # Clear cached branch in cl object, to avoid overwriting original CL branch
4933 # properties.
4934 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004935 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004936 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004937 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004938 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004939 return rtn
4940
wychen@chromium.org06928532015-02-03 02:11:29 +00004941 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004942 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07004943 cmd = ['git', 'diff']
4944 if options.stat:
4945 cmd.append('--stat')
4946 cmd.extend([TMP_BRANCH, branch, '--'])
4947 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004948 finally:
4949 RunGit(['checkout', '-q', branch])
4950 RunGit(['branch', '-D', TMP_BRANCH])
4951
4952 return 0
4953
4954
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004955def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004956 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004957 parser.add_option(
4958 '--no-color',
4959 action='store_true',
4960 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004961 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004962 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004963 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004964
4965 author = RunGit(['config', 'user.email']).strip() or None
4966
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004967 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004968
4969 if args:
4970 if len(args) > 1:
4971 parser.error('Unknown args')
4972 base_branch = args[0]
4973 else:
4974 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004975 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004976
4977 change = cl.GetChange(base_branch, None)
4978 return owners_finder.OwnersFinder(
4979 [f.LocalPath() for f in
4980 cl.GetChange(base_branch, None).AffectedFiles()],
4981 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004982 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004983 disable_color=options.no_color).run()
4984
4985
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004986def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004987 """Generates a diff command."""
4988 # Generate diff for the current branch's changes.
4989 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004990 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004991
4992 if args:
4993 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004994 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004995 diff_cmd.append(arg)
4996 else:
4997 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004998
4999 return diff_cmd
5000
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005001
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005002def MatchingFileType(file_name, extensions):
5003 """Returns true if the file name ends with one of the given extensions."""
5004 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005005
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005006
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:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005145 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005146 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)