blob: 4b58c7141a99aebc7c04f686261d1f4e49a42096 [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 Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000032import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000033import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000034import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000035import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000036import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037
38try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080039 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000040except ImportError:
41 pass
42
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000043from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000044from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000046import auth
skobes6468b902016-10-24 08:45:10 -070047import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000048import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000049import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000050import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000051import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000052import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000053import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000054import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000056import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000057import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000058import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000060import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040062import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000063import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000064import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065import watchlists
66
tandrii7400cf02016-06-21 08:48:07 -070067__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000068
tandrii9d2c7a32016-06-22 03:42:45 -070069COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070070DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080071POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000073REFS_THAT_ALIAS_TO_OTHER_REFS = {
74 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
75 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
76}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000077
thestig@chromium.org44202a22014-03-11 19:22:18 +000078# Valid extensions for files we want to lint.
79DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
80DEFAULT_LINT_IGNORE_REGEX = r"$^"
81
borenet6c0efe62016-10-19 08:13:29 -070082# Buildbucket master name prefix.
83MASTER_PREFIX = 'master.'
84
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000085# Shortcut since it quickly becomes redundant.
86Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000087
maruel@chromium.orgddd59412011-11-30 14:20:38 +000088# Initialized in main()
89settings = None
90
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010091# Used by tests/git_cl_test.py to add extra logging.
92# Inside the weirdly failing test, add this:
93# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
94# And scroll up to see the strack trace printed.
95_IS_BEING_TESTED = False
96
maruel@chromium.orgddd59412011-11-30 14:20:38 +000097
Christopher Lamf732cd52017-01-24 12:40:11 +110098def DieWithError(message, change_desc=None):
99 if change_desc:
100 SaveDescriptionBackup(change_desc)
101
vapiera7fbd5a2016-06-16 09:17:49 -0700102 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000103 sys.exit(1)
104
105
Christopher Lamf732cd52017-01-24 12:40:11 +1100106def SaveDescriptionBackup(change_desc):
107 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
108 print('\nError after CL description prompt -- saving description to %s\n' %
109 backup_path)
110 backup_file = open(backup_path, 'w')
111 backup_file.write(change_desc.description)
112 backup_file.close()
113
114
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000115def GetNoGitPagerEnv():
116 env = os.environ.copy()
117 # 'cat' is a magical git string that disables pagers on all platforms.
118 env['GIT_PAGER'] = 'cat'
119 return env
120
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000121
bsep@chromium.org627d9002016-04-29 00:00:52 +0000122def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000123 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000124 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000125 except subprocess2.CalledProcessError as e:
126 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000127 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000129 'Command "%s" failed.\n%s' % (
130 ' '.join(args), error_message or e.stdout or ''))
131 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000132
133
134def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000135 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000136 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000137
138
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000139def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000140 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700141 if suppress_stderr:
142 stderr = subprocess2.VOID
143 else:
144 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000145 try:
tandrii5d48c322016-08-18 16:19:37 -0700146 (out, _), code = subprocess2.communicate(['git'] + args,
147 env=GetNoGitPagerEnv(),
148 stdout=subprocess2.PIPE,
149 stderr=stderr)
150 return code, out
151 except subprocess2.CalledProcessError as e:
152 logging.debug('Failed running %s', args)
153 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000154
155
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000156def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000157 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000158 return RunGitWithCode(args, suppress_stderr=True)[1]
159
160
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000161def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000162 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000163 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000164 return (version.startswith(prefix) and
165 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000166
167
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000168def BranchExists(branch):
169 """Return True if specified branch exists."""
170 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
171 suppress_stderr=True)
172 return not code
173
174
tandrii2a16b952016-10-19 07:09:44 -0700175def time_sleep(seconds):
176 # Use this so that it can be mocked in tests without interfering with python
177 # system machinery.
178 import time # Local import to discourage others from importing time globally.
179 return time.sleep(seconds)
180
181
maruel@chromium.org90541732011-04-01 17:54:18 +0000182def ask_for_data(prompt):
183 try:
184 return raw_input(prompt)
185 except KeyboardInterrupt:
186 # Hide the exception.
187 sys.exit(1)
188
189
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100190def confirm_or_exit(prefix='', action='confirm'):
191 """Asks user to press enter to continue or press Ctrl+C to abort."""
192 if not prefix or prefix.endswith('\n'):
193 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100194 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100195 mid = ' Press'
196 elif prefix.endswith(' '):
197 mid = 'press'
198 else:
199 mid = ' press'
200 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
201
202
203def ask_for_explicit_yes(prompt):
204 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
205 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
206 while True:
207 if 'yes'.startswith(result):
208 return True
209 if 'no'.startswith(result):
210 return False
211 result = ask_for_data('Please, type yes or no: ').lower()
212
213
tandrii5d48c322016-08-18 16:19:37 -0700214def _git_branch_config_key(branch, key):
215 """Helper method to return Git config key for a branch."""
216 assert branch, 'branch name is required to set git config for it'
217 return 'branch.%s.%s' % (branch, key)
218
219
220def _git_get_branch_config_value(key, default=None, value_type=str,
221 branch=False):
222 """Returns git config value of given or current branch if any.
223
224 Returns default in all other cases.
225 """
226 assert value_type in (int, str, bool)
227 if branch is False: # Distinguishing default arg value from None.
228 branch = GetCurrentBranch()
229
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000230 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700231 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000232
tandrii5d48c322016-08-18 16:19:37 -0700233 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700234 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700235 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700236 # git config also has --int, but apparently git config suffers from integer
237 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700238 args.append(_git_branch_config_key(branch, key))
239 code, out = RunGitWithCode(args)
240 if code == 0:
241 value = out.strip()
242 if value_type == int:
243 return int(value)
244 if value_type == bool:
245 return bool(value.lower() == 'true')
246 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000247 return default
248
249
tandrii5d48c322016-08-18 16:19:37 -0700250def _git_set_branch_config_value(key, value, branch=None, **kwargs):
251 """Sets the value or unsets if it's None of a git branch config.
252
253 Valid, though not necessarily existing, branch must be provided,
254 otherwise currently checked out branch is used.
255 """
256 if not branch:
257 branch = GetCurrentBranch()
258 assert branch, 'a branch name OR currently checked out branch is required'
259 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700260 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700261 if value is None:
262 args.append('--unset')
263 elif isinstance(value, bool):
264 args.append('--bool')
265 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700266 else:
tandrii33a46ff2016-08-23 05:53:40 -0700267 # git config also has --int, but apparently git config suffers from integer
268 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700269 value = str(value)
270 args.append(_git_branch_config_key(branch, key))
271 if value is not None:
272 args.append(value)
273 RunGit(args, **kwargs)
274
275
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100276def _get_committer_timestamp(commit):
277 """Returns unix timestamp as integer of a committer in a commit.
278
279 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
280 """
281 # Git also stores timezone offset, but it only affects visual display,
282 # actual point in time is defined by this timestamp only.
283 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
284
285
286def _git_amend_head(message, committer_timestamp):
287 """Amends commit with new message and desired committer_timestamp.
288
289 Sets committer timezone to UTC.
290 """
291 env = os.environ.copy()
292 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
293 return RunGit(['commit', '--amend', '-m', message], env=env)
294
295
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000296def add_git_similarity(parser):
297 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700298 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000299 help='Sets the percentage that a pair of files need to match in order to'
300 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000301 parser.add_option(
302 '--find-copies', action='store_true',
303 help='Allows git to look for copies.')
304 parser.add_option(
305 '--no-find-copies', action='store_false', dest='find_copies',
306 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000307
308 old_parser_args = parser.parse_args
309 def Parse(args):
310 options, args = old_parser_args(args)
311
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000312 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700313 options.similarity = _git_get_branch_config_value(
314 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000315 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000316 print('Note: Saving similarity of %d%% in git config.'
317 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700318 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000319
iannucci@chromium.org79540052012-10-19 23:15:26 +0000320 options.similarity = max(0, min(options.similarity, 100))
321
322 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700323 options.find_copies = _git_get_branch_config_value(
324 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000325 else:
tandrii5d48c322016-08-18 16:19:37 -0700326 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000327
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000328 return options, args
329 parser.parse_args = Parse
330
331
machenbach@chromium.org45453142015-09-15 08:45:22 +0000332def _get_properties_from_options(options):
333 properties = dict(x.split('=', 1) for x in options.properties)
334 for key, val in properties.iteritems():
335 try:
336 properties[key] = json.loads(val)
337 except ValueError:
338 pass # If a value couldn't be evaluated, treat it as a string.
339 return properties
340
341
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000342def _prefix_master(master):
343 """Convert user-specified master name to full master name.
344
345 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
346 name, while the developers always use shortened master name
347 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
348 function does the conversion for buildbucket migration.
349 """
borenet6c0efe62016-10-19 08:13:29 -0700350 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 return master
borenet6c0efe62016-10-19 08:13:29 -0700352 return '%s%s' % (MASTER_PREFIX, master)
353
354
355def _unprefix_master(bucket):
356 """Convert bucket name to shortened master name.
357
358 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
359 name, while the developers always use shortened master name
360 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
361 function does the conversion for buildbucket migration.
362 """
363 if bucket.startswith(MASTER_PREFIX):
364 return bucket[len(MASTER_PREFIX):]
365 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000366
367
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000368def _buildbucket_retry(operation_name, http, *args, **kwargs):
369 """Retries requests to buildbucket service and returns parsed json content."""
370 try_count = 0
371 while True:
372 response, content = http.request(*args, **kwargs)
373 try:
374 content_json = json.loads(content)
375 except ValueError:
376 content_json = None
377
378 # Buildbucket could return an error even if status==200.
379 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000380 error = content_json.get('error')
381 if error.get('code') == 403:
382 raise BuildbucketResponseException(
383 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000384 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000385 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000386 raise BuildbucketResponseException(msg)
387
388 if response.status == 200:
389 if not content_json:
390 raise BuildbucketResponseException(
391 'Buildbucket returns invalid json content: %s.\n'
392 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
393 content)
394 return content_json
395 if response.status < 500 or try_count >= 2:
396 raise httplib2.HttpLib2Error(content)
397
398 # status >= 500 means transient failures.
399 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700400 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000401 try_count += 1
402 assert False, 'unreachable'
403
404
qyearsley1fdfcb62016-10-24 13:22:03 -0700405def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700406 """Returns a dict mapping bucket names to builders and tests,
407 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700408 """
qyearsleydd49f942016-10-28 11:57:22 -0700409 # If no bots are listed, we try to get a set of builders and tests based
410 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700411 if not options.bot:
412 change = changelist.GetChange(
413 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700414 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700415 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700416 change=change,
417 changed_files=change.LocalPaths(),
418 repository_root=settings.GetRoot(),
419 default_presubmit=None,
420 project=None,
421 verbose=options.verbose,
422 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700423 if masters is None:
424 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100425 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700426
qyearsley1fdfcb62016-10-24 13:22:03 -0700427 if options.bucket:
428 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700429 if options.master:
430 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700431
qyearsleydd49f942016-10-28 11:57:22 -0700432 # If bots are listed but no master or bucket, then we need to find out
433 # the corresponding master for each bot.
434 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
435 if error_message:
436 option_parser.error(
437 'Tryserver master cannot be found because: %s\n'
438 'Please manually specify the tryserver master, e.g. '
439 '"-m tryserver.chromium.linux".' % error_message)
440 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700441
442
qyearsley123a4682016-10-26 09:12:17 -0700443def _get_bucket_map_for_builders(builders):
444 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700445 map_url = 'https://builders-map.appspot.com/'
446 try:
qyearsley123a4682016-10-26 09:12:17 -0700447 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700448 except urllib2.URLError as e:
449 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
450 (map_url, e))
451 except ValueError as e:
452 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700453 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700454 return None, 'Failed to build master map.'
455
qyearsley123a4682016-10-26 09:12:17 -0700456 bucket_map = {}
457 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700458 masters = builders_map.get(builder, [])
459 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700460 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700461 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700462 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700463 (builder, masters))
464 bucket = _prefix_master(masters[0])
465 bucket_map.setdefault(bucket, {})[builder] = []
466
467 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700468
469
borenet6c0efe62016-10-19 08:13:29 -0700470def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700471 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700472 """Sends a request to Buildbucket to trigger try jobs for a changelist.
473
474 Args:
475 auth_config: AuthConfig for Rietveld.
476 changelist: Changelist that the try jobs are associated with.
477 buckets: A nested dict mapping bucket names to builders to tests.
478 options: Command-line options.
479 """
tandriide281ae2016-10-12 06:02:30 -0700480 assert changelist.GetIssue(), 'CL must be uploaded first'
481 codereview_url = changelist.GetCodereviewServer()
482 assert codereview_url, 'CL must be uploaded first'
483 patchset = patchset or changelist.GetMostRecentPatchset()
484 assert patchset, 'CL must be uploaded first'
485
486 codereview_host = urlparse.urlparse(codereview_url).hostname
487 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000488 http = authenticator.authorize(httplib2.Http())
489 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700490
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000491 buildbucket_put_url = (
492 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000493 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700494 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
495 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
496 hostname=codereview_host,
497 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000498 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700499
500 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
501 shared_parameters_properties['category'] = category
502 if options.clobber:
503 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700504 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700505 if extra_properties:
506 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000507
508 batch_req_body = {'builds': []}
509 print_text = []
510 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700511 for bucket, builders_and_tests in sorted(buckets.iteritems()):
512 print_text.append('Bucket: %s' % bucket)
513 master = None
514 if bucket.startswith(MASTER_PREFIX):
515 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000516 for builder, tests in sorted(builders_and_tests.iteritems()):
517 print_text.append(' %s: %s' % (builder, tests))
518 parameters = {
519 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000520 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100521 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000522 'revision': options.revision,
523 }],
tandrii8c5a3532016-11-04 07:52:02 -0700524 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000525 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000526 if 'presubmit' in builder.lower():
527 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000528 if tests:
529 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700530
531 tags = [
532 'builder:%s' % builder,
533 'buildset:%s' % buildset,
534 'user_agent:git_cl_try',
535 ]
536 if master:
537 parameters['properties']['master'] = master
538 tags.append('master:%s' % master)
539
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000540 batch_req_body['builds'].append(
541 {
542 'bucket': bucket,
543 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700545 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000546 }
547 )
548
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000549 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700550 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 http,
552 buildbucket_put_url,
553 'PUT',
554 body=json.dumps(batch_req_body),
555 headers={'Content-Type': 'application/json'}
556 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000557 print_text.append('To see results here, run: git cl try-results')
558 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700559 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000560
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000561
tandrii221ab252016-10-06 08:12:04 -0700562def fetch_try_jobs(auth_config, changelist, buildbucket_host,
563 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700564 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565
qyearsley53f48a12016-09-01 10:45:13 -0700566 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567 """
tandrii221ab252016-10-06 08:12:04 -0700568 assert buildbucket_host
569 assert changelist.GetIssue(), 'CL must be uploaded first'
570 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
571 patchset = patchset or changelist.GetMostRecentPatchset()
572 assert patchset, 'CL must be uploaded first'
573
574 codereview_url = changelist.GetCodereviewServer()
575 codereview_host = urlparse.urlparse(codereview_url).hostname
576 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577 if authenticator.has_cached_credentials():
578 http = authenticator.authorize(httplib2.Http())
579 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700580 print('Warning: Some results might be missing because %s' %
581 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700582 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000583 http = httplib2.Http()
584
585 http.force_exception_to_status_code = True
586
tandrii221ab252016-10-06 08:12:04 -0700587 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
588 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
589 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000590 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700591 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000592 params = {'tag': 'buildset:%s' % buildset}
593
594 builds = {}
595 while True:
596 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700597 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000598 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700599 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000600 for build in content.get('builds', []):
601 builds[build['id']] = build
602 if 'next_cursor' in content:
603 params['start_cursor'] = content['next_cursor']
604 else:
605 break
606 return builds
607
608
qyearsleyeab3c042016-08-24 09:18:28 -0700609def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000610 """Prints nicely result of fetch_try_jobs."""
611 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700612 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000613 return
614
615 # Make a copy, because we'll be modifying builds dictionary.
616 builds = builds.copy()
617 builder_names_cache = {}
618
619 def get_builder(b):
620 try:
621 return builder_names_cache[b['id']]
622 except KeyError:
623 try:
624 parameters = json.loads(b['parameters_json'])
625 name = parameters['builder_name']
626 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700627 print('WARNING: failed to get builder name for build %s: %s' % (
628 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000629 name = None
630 builder_names_cache[b['id']] = name
631 return name
632
633 def get_bucket(b):
634 bucket = b['bucket']
635 if bucket.startswith('master.'):
636 return bucket[len('master.'):]
637 return bucket
638
639 if options.print_master:
640 name_fmt = '%%-%ds %%-%ds' % (
641 max(len(str(get_bucket(b))) for b in builds.itervalues()),
642 max(len(str(get_builder(b))) for b in builds.itervalues()))
643 def get_name(b):
644 return name_fmt % (get_bucket(b), get_builder(b))
645 else:
646 name_fmt = '%%-%ds' % (
647 max(len(str(get_builder(b))) for b in builds.itervalues()))
648 def get_name(b):
649 return name_fmt % get_builder(b)
650
651 def sort_key(b):
652 return b['status'], b.get('result'), get_name(b), b.get('url')
653
654 def pop(title, f, color=None, **kwargs):
655 """Pop matching builds from `builds` dict and print them."""
656
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000657 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000658 colorize = str
659 else:
660 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
661
662 result = []
663 for b in builds.values():
664 if all(b.get(k) == v for k, v in kwargs.iteritems()):
665 builds.pop(b['id'])
666 result.append(b)
667 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700668 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000669 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700670 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000671
672 total = len(builds)
673 pop(status='COMPLETED', result='SUCCESS',
674 title='Successes:', color=Fore.GREEN,
675 f=lambda b: (get_name(b), b.get('url')))
676 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
677 title='Infra Failures:', color=Fore.MAGENTA,
678 f=lambda b: (get_name(b), b.get('url')))
679 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
680 title='Failures:', color=Fore.RED,
681 f=lambda b: (get_name(b), b.get('url')))
682 pop(status='COMPLETED', result='CANCELED',
683 title='Canceled:', color=Fore.MAGENTA,
684 f=lambda b: (get_name(b),))
685 pop(status='COMPLETED', result='FAILURE',
686 failure_reason='INVALID_BUILD_DEFINITION',
687 title='Wrong master/builder name:', color=Fore.MAGENTA,
688 f=lambda b: (get_name(b),))
689 pop(status='COMPLETED', result='FAILURE',
690 title='Other failures:',
691 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
692 pop(status='COMPLETED',
693 title='Other finished:',
694 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
695 pop(status='STARTED',
696 title='Started:', color=Fore.YELLOW,
697 f=lambda b: (get_name(b), b.get('url')))
698 pop(status='SCHEDULED',
699 title='Scheduled:',
700 f=lambda b: (get_name(b), 'id=%s' % b['id']))
701 # The last section is just in case buildbucket API changes OR there is a bug.
702 pop(title='Other:',
703 f=lambda b: (get_name(b), 'id=%s' % b['id']))
704 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700705 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000706
707
qyearsley53f48a12016-09-01 10:45:13 -0700708def write_try_results_json(output_file, builds):
709 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
710
711 The input |builds| dict is assumed to be generated by Buildbucket.
712 Buildbucket documentation: http://goo.gl/G0s101
713 """
714
715 def convert_build_dict(build):
716 return {
717 'buildbucket_id': build.get('id'),
718 'status': build.get('status'),
719 'result': build.get('result'),
720 'bucket': build.get('bucket'),
721 'builder_name': json.loads(
722 build.get('parameters_json', '{}')).get('builder_name'),
723 'failure_reason': build.get('failure_reason'),
724 'url': build.get('url'),
725 }
726
727 converted = []
728 for _, build in sorted(builds.items()):
729 converted.append(convert_build_dict(build))
730 write_json(output_file, converted)
731
732
iannucci@chromium.org79540052012-10-19 23:15:26 +0000733def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000734 """Prints statistics about the change to the user."""
735 # --no-ext-diff is broken in some versions of Git, so try to work around
736 # this by overriding the environment (but there is still a problem if the
737 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000739 if 'GIT_EXTERNAL_DIFF' in env:
740 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000741
742 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800743 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000744 else:
745 similarity_options = ['-M%s' % similarity]
746
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000747 try:
748 stdout = sys.stdout.fileno()
749 except AttributeError:
750 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000751 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000752 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000753 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000754 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000755
756
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000757class BuildbucketResponseException(Exception):
758 pass
759
760
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000761class Settings(object):
762 def __init__(self):
763 self.default_server = None
764 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000765 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766 self.tree_status_url = None
767 self.viewvc_url = None
768 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000769 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000770 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000771 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000772 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000773 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000774 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775
776 def LazyUpdateIfNeeded(self):
777 """Updates the settings from a codereview.settings file, if available."""
778 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000779 # The only value that actually changes the behavior is
780 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000781 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000782 error_ok=True
783 ).strip().lower()
784
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000786 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 LoadCodereviewSettingsFromFile(cr_settings_file)
788 self.updated = True
789
790 def GetDefaultServerUrl(self, error_ok=False):
791 if not self.default_server:
792 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000793 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 if error_ok:
796 return self.default_server
797 if not self.default_server:
798 error_message = ('Could not find settings file. You must configure '
799 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000800 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000801 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802 return self.default_server
803
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000804 @staticmethod
805 def GetRelativeRoot():
806 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000807
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000809 if self.root is None:
810 self.root = os.path.abspath(self.GetRelativeRoot())
811 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000813 def GetGitMirror(self, remote='origin'):
814 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000815 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000816 if not os.path.isdir(local_url):
817 return None
818 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
819 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100820 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100821 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000822 if mirror.exists():
823 return mirror
824 return None
825
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 def GetTreeStatusUrl(self, error_ok=False):
827 if not self.tree_status_url:
828 error_message = ('You must configure your tree status URL by running '
829 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000830 self.tree_status_url = self._GetRietveldConfig(
831 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832 return self.tree_status_url
833
834 def GetViewVCUrl(self):
835 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837 return self.viewvc_url
838
rmistry@google.com90752582014-01-14 21:04:50 +0000839 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000840 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000841
rmistry@google.com78948ed2015-07-08 23:09:57 +0000842 def GetIsSkipDependencyUpload(self, branch_name):
843 """Returns true if specified branch should skip dep uploads."""
844 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
845 error_ok=True)
846
rmistry@google.com5626a922015-02-26 14:03:30 +0000847 def GetRunPostUploadHook(self):
848 run_post_upload_hook = self._GetRietveldConfig(
849 'run-post-upload-hook', error_ok=True)
850 return run_post_upload_hook == "True"
851
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000852 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000853 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000854
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000855 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000856 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000857
ukai@chromium.orge8077812012-02-03 03:41:46 +0000858 def GetIsGerrit(self):
859 """Return true if this repo is assosiated with gerrit code review system."""
860 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700861 self.is_gerrit = (
862 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000863 return self.is_gerrit
864
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000865 def GetSquashGerritUploads(self):
866 """Return true if uploads to Gerrit should be squashed by default."""
867 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700868 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
869 if self.squash_gerrit_uploads is None:
870 # Default is squash now (http://crbug.com/611892#c23).
871 self.squash_gerrit_uploads = not (
872 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
873 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000874 return self.squash_gerrit_uploads
875
tandriia60502f2016-06-20 02:01:53 -0700876 def GetSquashGerritUploadsOverride(self):
877 """Return True or False if codereview.settings should be overridden.
878
879 Returns None if no override has been defined.
880 """
881 # See also http://crbug.com/611892#c23
882 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
883 error_ok=True).strip()
884 if result == 'true':
885 return True
886 if result == 'false':
887 return False
888 return None
889
tandrii@chromium.org28253532016-04-14 13:46:56 +0000890 def GetGerritSkipEnsureAuthenticated(self):
891 """Return True if EnsureAuthenticated should not be done for Gerrit
892 uploads."""
893 if self.gerrit_skip_ensure_authenticated is None:
894 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000895 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000896 error_ok=True).strip() == 'true')
897 return self.gerrit_skip_ensure_authenticated
898
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000899 def GetGitEditor(self):
900 """Return the editor specified in the git config, or None if none is."""
901 if self.git_editor is None:
902 self.git_editor = self._GetConfig('core.editor', error_ok=True)
903 return self.git_editor or None
904
thestig@chromium.org44202a22014-03-11 19:22:18 +0000905 def GetLintRegex(self):
906 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
907 DEFAULT_LINT_REGEX)
908
909 def GetLintIgnoreRegex(self):
910 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
911 DEFAULT_LINT_IGNORE_REGEX)
912
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000913 def GetProject(self):
914 if not self.project:
915 self.project = self._GetRietveldConfig('project', error_ok=True)
916 return self.project
917
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000918 def _GetRietveldConfig(self, param, **kwargs):
919 return self._GetConfig('rietveld.' + param, **kwargs)
920
rmistry@google.com78948ed2015-07-08 23:09:57 +0000921 def _GetBranchConfig(self, branch_name, param, **kwargs):
922 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
923
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000924 def _GetConfig(self, param, **kwargs):
925 self.LazyUpdateIfNeeded()
926 return RunGit(['config', param], **kwargs).strip()
927
928
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100929@contextlib.contextmanager
930def _get_gerrit_project_config_file(remote_url):
931 """Context manager to fetch and store Gerrit's project.config from
932 refs/meta/config branch and store it in temp file.
933
934 Provides a temporary filename or None if there was error.
935 """
936 error, _ = RunGitWithCode([
937 'fetch', remote_url,
938 '+refs/meta/config:refs/git_cl/meta/config'])
939 if error:
940 # Ref doesn't exist or isn't accessible to current user.
941 print('WARNING: failed to fetch project config for %s: %s' %
942 (remote_url, error))
943 yield None
944 return
945
946 error, project_config_data = RunGitWithCode(
947 ['show', 'refs/git_cl/meta/config:project.config'])
948 if error:
949 print('WARNING: project.config file not found')
950 yield None
951 return
952
953 with gclient_utils.temporary_directory() as tempdir:
954 project_config_file = os.path.join(tempdir, 'project.config')
955 gclient_utils.FileWrite(project_config_file, project_config_data)
956 yield project_config_file
957
958
959def _is_git_numberer_enabled(remote_url, remote_ref):
960 """Returns True if Git Numberer is enabled on this ref."""
961 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100962 KNOWN_PROJECTS_WHITELIST = [
963 'chromium/src',
964 'external/webrtc',
965 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100966 'infra/experimental',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100967 ]
968
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100969 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
970 url_parts = urlparse.urlparse(remote_url)
971 project_name = url_parts.path.lstrip('/').rstrip('git./')
972 for known in KNOWN_PROJECTS_WHITELIST:
973 if project_name.endswith(known):
974 break
975 else:
976 # Early exit to avoid extra fetches for repos that aren't using Git
977 # Numberer.
978 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100979
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100980 with _get_gerrit_project_config_file(remote_url) as project_config_file:
981 if project_config_file is None:
982 # Failed to fetch project.config, which shouldn't happen on open source
983 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100984 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100985 def get_opts(x):
986 code, out = RunGitWithCode(
987 ['config', '-f', project_config_file, '--get-all',
988 'plugin.git-numberer.validate-%s-refglob' % x])
989 if code == 0:
990 return out.strip().splitlines()
991 return []
992 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100993
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100994 logging.info('validator config enabled %s disabled %s refglobs for '
995 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000996
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100997 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100998 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100999 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001000 return True
1001 return False
1002
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001003 if match_refglobs(disabled):
1004 return False
1005 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001006
1007
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008def ShortBranchName(branch):
1009 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001010 return branch.replace('refs/heads/', '', 1)
1011
1012
1013def GetCurrentBranchRef():
1014 """Returns branch ref (e.g., refs/heads/master) or None."""
1015 return RunGit(['symbolic-ref', 'HEAD'],
1016 stderr=subprocess2.VOID, error_ok=True).strip() or None
1017
1018
1019def GetCurrentBranch():
1020 """Returns current branch or None.
1021
1022 For refs/heads/* branches, returns just last part. For others, full ref.
1023 """
1024 branchref = GetCurrentBranchRef()
1025 if branchref:
1026 return ShortBranchName(branchref)
1027 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001028
1029
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001030class _CQState(object):
1031 """Enum for states of CL with respect to Commit Queue."""
1032 NONE = 'none'
1033 DRY_RUN = 'dry_run'
1034 COMMIT = 'commit'
1035
1036 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1037
1038
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001039class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001040 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001041 self.issue = issue
1042 self.patchset = patchset
1043 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001044 assert codereview in (None, 'rietveld', 'gerrit')
1045 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001046
1047 @property
1048 def valid(self):
1049 return self.issue is not None
1050
1051
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001052def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001053 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1054 fail_result = _ParsedIssueNumberArgument()
1055
1056 if arg.isdigit():
1057 return _ParsedIssueNumberArgument(issue=int(arg))
1058 if not arg.startswith('http'):
1059 return fail_result
1060 url = gclient_utils.UpgradeToHttps(arg)
1061 try:
1062 parsed_url = urlparse.urlparse(url)
1063 except ValueError:
1064 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001065
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001066 if codereview is not None:
1067 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1068 return parsed or fail_result
1069
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001070 results = {}
1071 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1072 parsed = cls.ParseIssueURL(parsed_url)
1073 if parsed is not None:
1074 results[name] = parsed
1075
1076 if not results:
1077 return fail_result
1078 if len(results) == 1:
1079 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001080
1081 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1082 # This is likely Gerrit.
1083 return results['gerrit']
1084 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001085 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001086
1087
Aaron Gablea45ee112016-11-22 15:14:38 -08001088class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001089 def __init__(self, issue, url):
1090 self.issue = issue
1091 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001092 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001093
1094 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001095 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001096 self.issue, self.url)
1097
1098
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001099_CommentSummary = collections.namedtuple(
1100 '_CommentSummary', ['date', 'message', 'sender',
1101 # TODO(tandrii): these two aren't known in Gerrit.
1102 'approval', 'disapproval'])
1103
1104
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001106 """Changelist works with one changelist in local branch.
1107
1108 Supports two codereview backends: Rietveld or Gerrit, selected at object
1109 creation.
1110
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001111 Notes:
1112 * Not safe for concurrent multi-{thread,process} use.
1113 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001114 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001115 """
1116
1117 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1118 """Create a new ChangeList instance.
1119
1120 If issue is given, the codereview must be given too.
1121
1122 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1123 Otherwise, it's decided based on current configuration of the local branch,
1124 with default being 'rietveld' for backwards compatibility.
1125 See _load_codereview_impl for more details.
1126
1127 **kwargs will be passed directly to codereview implementation.
1128 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001130 global settings
1131 if not settings:
1132 # Happens when git_cl.py is used as a utility library.
1133 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001134
1135 if issue:
1136 assert codereview, 'codereview must be known, if issue is known'
1137
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 self.branchref = branchref
1139 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001140 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 self.branch = ShortBranchName(self.branchref)
1142 else:
1143 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001144 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001145 self.lookedup_issue = False
1146 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 self.has_description = False
1148 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001149 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001151 self.cc = None
1152 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001153 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001154
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001155 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001156 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001157 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001158 assert self._codereview_impl
1159 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001160
1161 def _load_codereview_impl(self, codereview=None, **kwargs):
1162 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001163 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1164 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1165 self._codereview = codereview
1166 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001167 return
1168
1169 # Automatic selection based on issue number set for a current branch.
1170 # Rietveld takes precedence over Gerrit.
1171 assert not self.issue
1172 # Whether we find issue or not, we are doing the lookup.
1173 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001174 if self.GetBranch():
1175 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1176 issue = _git_get_branch_config_value(
1177 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1178 if issue:
1179 self._codereview = codereview
1180 self._codereview_impl = cls(self, **kwargs)
1181 self.issue = int(issue)
1182 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001183
1184 # No issue is set for this branch, so decide based on repo-wide settings.
1185 return self._load_codereview_impl(
1186 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1187 **kwargs)
1188
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001189 def IsGerrit(self):
1190 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001191
1192 def GetCCList(self):
1193 """Return the users cc'd on this CL.
1194
agable92bec4f2016-08-24 09:27:27 -07001195 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001196 """
1197 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001198 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001199 more_cc = ','.join(self.watchers)
1200 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1201 return self.cc
1202
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001203 def GetCCListWithoutDefault(self):
1204 """Return the users cc'd on this CL excluding default ones."""
1205 if self.cc is None:
1206 self.cc = ','.join(self.watchers)
1207 return self.cc
1208
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001209 def SetWatchers(self, watchers):
1210 """Set the list of email addresses that should be cc'd based on the changed
1211 files in this CL.
1212 """
1213 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214
1215 def GetBranch(self):
1216 """Returns the short branch name, e.g. 'master'."""
1217 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001218 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001219 if not branchref:
1220 return None
1221 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 self.branch = ShortBranchName(self.branchref)
1223 return self.branch
1224
1225 def GetBranchRef(self):
1226 """Returns the full branch name, e.g. 'refs/heads/master'."""
1227 self.GetBranch() # Poke the lazy loader.
1228 return self.branchref
1229
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001230 def ClearBranch(self):
1231 """Clears cached branch data of this object."""
1232 self.branch = self.branchref = None
1233
tandrii5d48c322016-08-18 16:19:37 -07001234 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1235 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1236 kwargs['branch'] = self.GetBranch()
1237 return _git_get_branch_config_value(key, default, **kwargs)
1238
1239 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1240 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1241 assert self.GetBranch(), (
1242 'this CL must have an associated branch to %sset %s%s' %
1243 ('un' if value is None else '',
1244 key,
1245 '' if value is None else ' to %r' % value))
1246 kwargs['branch'] = self.GetBranch()
1247 return _git_set_branch_config_value(key, value, **kwargs)
1248
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001249 @staticmethod
1250 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001251 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 e.g. 'origin', 'refs/heads/master'
1253 """
1254 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001255 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1256
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001258 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001260 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1261 error_ok=True).strip()
1262 if upstream_branch:
1263 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001265 # Else, try to guess the origin remote.
1266 remote_branches = RunGit(['branch', '-r']).split()
1267 if 'origin/master' in remote_branches:
1268 # Fall back on origin/master if it exits.
1269 remote = 'origin'
1270 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001272 DieWithError(
1273 'Unable to determine default branch to diff against.\n'
1274 'Either pass complete "git diff"-style arguments, like\n'
1275 ' git cl upload origin/master\n'
1276 'or verify this branch is set up to track another \n'
1277 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278
1279 return remote, upstream_branch
1280
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001281 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001282 upstream_branch = self.GetUpstreamBranch()
1283 if not BranchExists(upstream_branch):
1284 DieWithError('The upstream for the current branch (%s) does not exist '
1285 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001286 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001287 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001288
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001289 def GetUpstreamBranch(self):
1290 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001291 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001293 upstream_branch = upstream_branch.replace('refs/heads/',
1294 'refs/remotes/%s/' % remote)
1295 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1296 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001297 self.upstream_branch = upstream_branch
1298 return self.upstream_branch
1299
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001301 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001302 remote, branch = None, self.GetBranch()
1303 seen_branches = set()
1304 while branch not in seen_branches:
1305 seen_branches.add(branch)
1306 remote, branch = self.FetchUpstreamTuple(branch)
1307 branch = ShortBranchName(branch)
1308 if remote != '.' or branch.startswith('refs/remotes'):
1309 break
1310 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001311 remotes = RunGit(['remote'], error_ok=True).split()
1312 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001313 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001314 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001315 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001316 logging.warn('Could not determine which remote this change is '
1317 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001318 else:
1319 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001320 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001321 branch = 'HEAD'
1322 if branch.startswith('refs/remotes'):
1323 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001324 elif branch.startswith('refs/branch-heads/'):
1325 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001326 else:
1327 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001328 return self._remote
1329
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001330 def GitSanityChecks(self, upstream_git_obj):
1331 """Checks git repo status and ensures diff is from local commits."""
1332
sbc@chromium.org79706062015-01-14 21:18:12 +00001333 if upstream_git_obj is None:
1334 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001335 print('ERROR: unable to determine current branch (detached HEAD?)',
1336 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001337 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001338 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001339 return False
1340
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001341 # Verify the commit we're diffing against is in our current branch.
1342 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1343 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1344 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001345 print('ERROR: %s is not in the current branch. You may need to rebase '
1346 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001347 return False
1348
1349 # List the commits inside the diff, and verify they are all local.
1350 commits_in_diff = RunGit(
1351 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1352 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1353 remote_branch = remote_branch.strip()
1354 if code != 0:
1355 _, remote_branch = self.GetRemoteBranch()
1356
1357 commits_in_remote = RunGit(
1358 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1359
1360 common_commits = set(commits_in_diff) & set(commits_in_remote)
1361 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001362 print('ERROR: Your diff contains %d commits already in %s.\n'
1363 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1364 'the diff. If you are using a custom git flow, you can override'
1365 ' the reference used for this check with "git config '
1366 'gitcl.remotebranch <git-ref>".' % (
1367 len(common_commits), remote_branch, upstream_git_obj),
1368 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001369 return False
1370 return True
1371
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001372 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001373 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001374
1375 Returns None if it is not set.
1376 """
tandrii5d48c322016-08-18 16:19:37 -07001377 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001378
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 def GetRemoteUrl(self):
1380 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1381
1382 Returns None if there is no remote.
1383 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001384 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001385 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1386
1387 # If URL is pointing to a local directory, it is probably a git cache.
1388 if os.path.isdir(url):
1389 url = RunGit(['config', 'remote.%s.url' % remote],
1390 error_ok=True,
1391 cwd=url).strip()
1392 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001394 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001395 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001396 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001397 self.issue = self._GitGetBranchConfigValue(
1398 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001399 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400 return self.issue
1401
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 def GetIssueURL(self):
1403 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001404 issue = self.GetIssue()
1405 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001406 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001407 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001409 def GetDescription(self, pretty=False, force=False):
1410 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001412 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 self.has_description = True
1414 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001415 # Set width to 72 columns + 2 space indent.
1416 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001418 lines = self.description.splitlines()
1419 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 return self.description
1421
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001422 def GetDescriptionFooters(self):
1423 """Returns (non_footer_lines, footers) for the commit message.
1424
1425 Returns:
1426 non_footer_lines (list(str)) - Simple list of description lines without
1427 any footer. The lines do not contain newlines, nor does the list contain
1428 the empty line between the message and the footers.
1429 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1430 [("Change-Id", "Ideadbeef...."), ...]
1431 """
1432 raw_description = self.GetDescription()
1433 msg_lines, _, footers = git_footers.split_footers(raw_description)
1434 if footers:
1435 msg_lines = msg_lines[:len(msg_lines)-1]
1436 return msg_lines, footers
1437
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001438 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001439 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001440 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001441 self.patchset = self._GitGetBranchConfigValue(
1442 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001443 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444 return self.patchset
1445
1446 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001447 """Set this branch's patchset. If patchset=0, clears the patchset."""
1448 assert self.GetBranch()
1449 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001450 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001451 else:
1452 self.patchset = int(patchset)
1453 self._GitSetBranchConfigValue(
1454 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001456 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001457 """Set this branch's issue. If issue isn't given, clears the issue."""
1458 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001460 issue = int(issue)
1461 self._GitSetBranchConfigValue(
1462 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001463 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001464 codereview_server = self._codereview_impl.GetCodereviewServer()
1465 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001466 self._GitSetBranchConfigValue(
1467 self._codereview_impl.CodereviewServerConfigKey(),
1468 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001469 else:
tandrii5d48c322016-08-18 16:19:37 -07001470 # Reset all of these just to be clean.
1471 reset_suffixes = [
1472 'last-upload-hash',
1473 self._codereview_impl.IssueConfigKey(),
1474 self._codereview_impl.PatchsetConfigKey(),
1475 self._codereview_impl.CodereviewServerConfigKey(),
1476 ] + self._PostUnsetIssueProperties()
1477 for prop in reset_suffixes:
1478 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001479 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001480 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481
dnjba1b0f32016-09-02 12:37:42 -07001482 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001483 if not self.GitSanityChecks(upstream_branch):
1484 DieWithError('\nGit sanity check failure')
1485
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001486 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001487 if not root:
1488 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001489 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001490
1491 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001492 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001493 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001494 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001495 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001496 except subprocess2.CalledProcessError:
1497 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001498 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001499 'This branch probably doesn\'t exist anymore. To reset the\n'
1500 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001501 ' git branch --set-upstream-to origin/master %s\n'
1502 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001503 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001504
maruel@chromium.org52424302012-08-29 15:14:30 +00001505 issue = self.GetIssue()
1506 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001507 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001508 description = self.GetDescription()
1509 else:
1510 # If the change was never uploaded, use the log messages of all commits
1511 # up to the branch point, as git cl upload will prefill the description
1512 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001513 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1514 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001515
1516 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001517 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001518 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001519 name,
1520 description,
1521 absroot,
1522 files,
1523 issue,
1524 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001525 author,
1526 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001527
dsansomee2d6fd92016-09-08 00:10:47 -07001528 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001529 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001530 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001531 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001532
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001533 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1534 """Sets the description for this CL remotely.
1535
1536 You can get description_lines and footers with GetDescriptionFooters.
1537
1538 Args:
1539 description_lines (list(str)) - List of CL description lines without
1540 newline characters.
1541 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1542 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1543 `List-Of-Tokens`). It will be case-normalized so that each token is
1544 title-cased.
1545 """
1546 new_description = '\n'.join(description_lines)
1547 if footers:
1548 new_description += '\n'
1549 for k, v in footers:
1550 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1551 if not git_footers.FOOTER_PATTERN.match(foot):
1552 raise ValueError('Invalid footer %r' % foot)
1553 new_description += foot + '\n'
1554 self.UpdateDescription(new_description, force)
1555
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001556 def RunHook(self, committing, may_prompt, verbose, change):
1557 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1558 try:
1559 return presubmit_support.DoPresubmitChecks(change, committing,
1560 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1561 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001562 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1563 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001564 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001565 DieWithError(
1566 ('%s\nMaybe your depot_tools is out of date?\n'
1567 'If all fails, contact maruel@') % e)
1568
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001569 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1570 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001571 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1572 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001573 else:
1574 # Assume url.
1575 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1576 urlparse.urlparse(issue_arg))
1577 if not parsed_issue_arg or not parsed_issue_arg.valid:
1578 DieWithError('Failed to parse issue argument "%s". '
1579 'Must be an issue number or a valid URL.' % issue_arg)
1580 return self._codereview_impl.CMDPatchWithParsedIssue(
1581 parsed_issue_arg, reject, nocommit, directory)
1582
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001583 def CMDUpload(self, options, git_diff_args, orig_args):
1584 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001585 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001586 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001587 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001588 else:
1589 if self.GetBranch() is None:
1590 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1591
1592 # Default to diffing against common ancestor of upstream branch
1593 base_branch = self.GetCommonAncestorWithUpstream()
1594 git_diff_args = [base_branch, 'HEAD']
1595
Aaron Gablec4c40d12017-05-22 11:49:53 -07001596 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1597 if not self.IsGerrit() and not self.GetIssue():
1598 print('=====================================')
1599 print('NOTICE: Rietveld is being deprecated. '
1600 'You can upload changes to Gerrit with')
1601 print(' git cl upload --gerrit')
1602 print('or set Gerrit to be your default code review tool with')
1603 print(' git config gerrit.host true')
1604 print('=====================================')
1605
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001606 # Fast best-effort checks to abort before running potentially
1607 # expensive hooks if uploading is likely to fail anyway. Passing these
1608 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001609 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001610 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001611
1612 # Apply watchlists on upload.
1613 change = self.GetChange(base_branch, None)
1614 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1615 files = [f.LocalPath() for f in change.AffectedFiles()]
1616 if not options.bypass_watchlists:
1617 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1618
1619 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001620 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001621 # Set the reviewer list now so that presubmit checks can access it.
1622 change_description = ChangeDescription(change.FullDescriptionText())
1623 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001624 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001625 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001626 change)
1627 change.SetDescriptionText(change_description.description)
1628 hook_results = self.RunHook(committing=False,
1629 may_prompt=not options.force,
1630 verbose=options.verbose,
1631 change=change)
1632 if not hook_results.should_continue():
1633 return 1
1634 if not options.reviewers and hook_results.reviewers:
1635 options.reviewers = hook_results.reviewers.split(',')
1636
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001637 # TODO(tandrii): Checking local patchset against remote patchset is only
1638 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1639 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001640 latest_patchset = self.GetMostRecentPatchset()
1641 local_patchset = self.GetPatchset()
1642 if (latest_patchset and local_patchset and
1643 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001644 print('The last upload made from this repository was patchset #%d but '
1645 'the most recent patchset on the server is #%d.'
1646 % (local_patchset, latest_patchset))
1647 print('Uploading will still work, but if you\'ve uploaded to this '
1648 'issue from another machine or branch the patch you\'re '
1649 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001650 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001651
1652 print_stats(options.similarity, options.find_copies, git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001653 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001654 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001655 if options.use_commit_queue:
1656 self.SetCQState(_CQState.COMMIT)
1657 elif options.cq_dry_run:
1658 self.SetCQState(_CQState.DRY_RUN)
1659
tandrii5d48c322016-08-18 16:19:37 -07001660 _git_set_branch_config_value('last-upload-hash',
1661 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001662 # Run post upload hooks, if specified.
1663 if settings.GetRunPostUploadHook():
1664 presubmit_support.DoPostUploadExecuter(
1665 change,
1666 self,
1667 settings.GetRoot(),
1668 options.verbose,
1669 sys.stdout)
1670
1671 # Upload all dependencies if specified.
1672 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001673 print()
1674 print('--dependencies has been specified.')
1675 print('All dependent local branches will be re-uploaded.')
1676 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001677 # Remove the dependencies flag from args so that we do not end up in a
1678 # loop.
1679 orig_args.remove('--dependencies')
1680 ret = upload_branch_deps(self, orig_args)
1681 return ret
1682
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001683 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001684 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001685
1686 Issue must have been already uploaded and known.
1687 """
1688 assert new_state in _CQState.ALL_STATES
1689 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001690 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001691 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001692 return 0
1693 except KeyboardInterrupt:
1694 raise
1695 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001696 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001697 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001698 ' * Your project has no CQ,\n'
1699 ' * You don\'t have permission to change the CQ state,\n'
1700 ' * There\'s a bug in this code (see stack trace below).\n'
1701 'Consider specifying which bots to trigger manually or asking your '
1702 'project owners for permissions or contacting Chrome Infra at:\n'
1703 'https://www.chromium.org/infra\n\n' %
1704 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001705 # Still raise exception so that stack trace is printed.
1706 raise
1707
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001708 # Forward methods to codereview specific implementation.
1709
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001710 def AddComment(self, message):
1711 return self._codereview_impl.AddComment(message)
1712
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001713 def GetCommentsSummary(self):
1714 """Returns list of _CommentSummary for each comment.
1715
1716 Note: comments per file or per line are not included,
1717 only top-level comments are returned.
1718 """
1719 return self._codereview_impl.GetCommentsSummary()
1720
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001721 def CloseIssue(self):
1722 return self._codereview_impl.CloseIssue()
1723
1724 def GetStatus(self):
1725 return self._codereview_impl.GetStatus()
1726
1727 def GetCodereviewServer(self):
1728 return self._codereview_impl.GetCodereviewServer()
1729
tandriide281ae2016-10-12 06:02:30 -07001730 def GetIssueOwner(self):
1731 """Get owner from codereview, which may differ from this checkout."""
1732 return self._codereview_impl.GetIssueOwner()
1733
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001734 def GetMostRecentPatchset(self):
1735 return self._codereview_impl.GetMostRecentPatchset()
1736
tandriide281ae2016-10-12 06:02:30 -07001737 def CannotTriggerTryJobReason(self):
1738 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1739 return self._codereview_impl.CannotTriggerTryJobReason()
1740
tandrii8c5a3532016-11-04 07:52:02 -07001741 def GetTryjobProperties(self, patchset=None):
1742 """Returns dictionary of properties to launch tryjob."""
1743 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1744
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001745 def __getattr__(self, attr):
1746 # This is because lots of untested code accesses Rietveld-specific stuff
1747 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001748 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001749 # Note that child method defines __getattr__ as well, and forwards it here,
1750 # because _RietveldChangelistImpl is not cleaned up yet, and given
1751 # deprecation of Rietveld, it should probably be just removed.
1752 # Until that time, avoid infinite recursion by bypassing __getattr__
1753 # of implementation class.
1754 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001755
1756
1757class _ChangelistCodereviewBase(object):
1758 """Abstract base class encapsulating codereview specifics of a changelist."""
1759 def __init__(self, changelist):
1760 self._changelist = changelist # instance of Changelist
1761
1762 def __getattr__(self, attr):
1763 # Forward methods to changelist.
1764 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1765 # _RietveldChangelistImpl to avoid this hack?
1766 return getattr(self._changelist, attr)
1767
1768 def GetStatus(self):
1769 """Apply a rough heuristic to give a simple summary of an issue's review
1770 or CQ status, assuming adherence to a common workflow.
1771
1772 Returns None if no issue for this branch, or specific string keywords.
1773 """
1774 raise NotImplementedError()
1775
1776 def GetCodereviewServer(self):
1777 """Returns server URL without end slash, like "https://codereview.com"."""
1778 raise NotImplementedError()
1779
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001780 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001781 """Fetches and returns description from the codereview server."""
1782 raise NotImplementedError()
1783
tandrii5d48c322016-08-18 16:19:37 -07001784 @classmethod
1785 def IssueConfigKey(cls):
1786 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001787 raise NotImplementedError()
1788
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001789 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001790 def PatchsetConfigKey(cls):
1791 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001792 raise NotImplementedError()
1793
tandrii5d48c322016-08-18 16:19:37 -07001794 @classmethod
1795 def CodereviewServerConfigKey(cls):
1796 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001797 raise NotImplementedError()
1798
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001799 def _PostUnsetIssueProperties(self):
1800 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001801 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001802
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001803 def GetRieveldObjForPresubmit(self):
1804 # This is an unfortunate Rietveld-embeddedness in presubmit.
1805 # For non-Rietveld codereviews, this probably should return a dummy object.
1806 raise NotImplementedError()
1807
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001808 def GetGerritObjForPresubmit(self):
1809 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1810 return None
1811
dsansomee2d6fd92016-09-08 00:10:47 -07001812 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001813 """Update the description on codereview site."""
1814 raise NotImplementedError()
1815
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001816 def AddComment(self, message):
1817 """Posts a comment to the codereview site."""
1818 raise NotImplementedError()
1819
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001820 def GetCommentsSummary(self):
1821 raise NotImplementedError()
1822
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001823 def CloseIssue(self):
1824 """Closes the issue."""
1825 raise NotImplementedError()
1826
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001827 def GetMostRecentPatchset(self):
1828 """Returns the most recent patchset number from the codereview site."""
1829 raise NotImplementedError()
1830
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001831 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1832 directory):
1833 """Fetches and applies the issue.
1834
1835 Arguments:
1836 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1837 reject: if True, reject the failed patch instead of switching to 3-way
1838 merge. Rietveld only.
1839 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1840 only.
1841 directory: switch to directory before applying the patch. Rietveld only.
1842 """
1843 raise NotImplementedError()
1844
1845 @staticmethod
1846 def ParseIssueURL(parsed_url):
1847 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1848 failed."""
1849 raise NotImplementedError()
1850
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001851 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001852 """Best effort check that user is authenticated with codereview server.
1853
1854 Arguments:
1855 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001856 refresh: whether to attempt to refresh credentials. Ignored if not
1857 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001858 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001859 raise NotImplementedError()
1860
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001861 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001862 """Best effort check that uploading isn't supposed to fail for predictable
1863 reasons.
1864
1865 This method should raise informative exception if uploading shouldn't
1866 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001867
1868 Arguments:
1869 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001870 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001871 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001872
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001873 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001874 """Uploads a change to codereview."""
1875 raise NotImplementedError()
1876
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001877 def SetCQState(self, new_state):
1878 """Update the CQ state for latest patchset.
1879
1880 Issue must have been already uploaded and known.
1881 """
1882 raise NotImplementedError()
1883
tandriie113dfd2016-10-11 10:20:12 -07001884 def CannotTriggerTryJobReason(self):
1885 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1886 raise NotImplementedError()
1887
tandriide281ae2016-10-12 06:02:30 -07001888 def GetIssueOwner(self):
1889 raise NotImplementedError()
1890
tandrii8c5a3532016-11-04 07:52:02 -07001891 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001892 raise NotImplementedError()
1893
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001894
1895class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001896 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001897 super(_RietveldChangelistImpl, self).__init__(changelist)
1898 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001899 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001900 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001901
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001902 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001903 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001904 self._props = None
1905 self._rpc_server = None
1906
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001907 def GetCodereviewServer(self):
1908 if not self._rietveld_server:
1909 # If we're on a branch then get the server potentially associated
1910 # with that branch.
1911 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001912 self._rietveld_server = gclient_utils.UpgradeToHttps(
1913 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001914 if not self._rietveld_server:
1915 self._rietveld_server = settings.GetDefaultServerUrl()
1916 return self._rietveld_server
1917
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001918 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001919 """Best effort check that user is authenticated with Rietveld server."""
1920 if self._auth_config.use_oauth2:
1921 authenticator = auth.get_authenticator_for_host(
1922 self.GetCodereviewServer(), self._auth_config)
1923 if not authenticator.has_cached_credentials():
1924 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001925 if refresh:
1926 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001927
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001928 def EnsureCanUploadPatchset(self, force):
1929 # No checks for Rietveld because we are deprecating Rietveld.
1930 pass
1931
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001932 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001933 issue = self.GetIssue()
1934 assert issue
1935 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001936 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001937 except urllib2.HTTPError as e:
1938 if e.code == 404:
1939 DieWithError(
1940 ('\nWhile fetching the description for issue %d, received a '
1941 '404 (not found)\n'
1942 'error. It is likely that you deleted this '
1943 'issue on the server. If this is the\n'
1944 'case, please run\n\n'
1945 ' git cl issue 0\n\n'
1946 'to clear the association with the deleted issue. Then run '
1947 'this command again.') % issue)
1948 else:
1949 DieWithError(
1950 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1951 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001952 print('Warning: Failed to retrieve CL description due to network '
1953 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001954 return ''
1955
1956 def GetMostRecentPatchset(self):
1957 return self.GetIssueProperties()['patchsets'][-1]
1958
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001959 def GetIssueProperties(self):
1960 if self._props is None:
1961 issue = self.GetIssue()
1962 if not issue:
1963 self._props = {}
1964 else:
1965 self._props = self.RpcServer().get_issue_properties(issue, True)
1966 return self._props
1967
tandriie113dfd2016-10-11 10:20:12 -07001968 def CannotTriggerTryJobReason(self):
1969 props = self.GetIssueProperties()
1970 if not props:
1971 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1972 if props.get('closed'):
1973 return 'CL %s is closed' % self.GetIssue()
1974 if props.get('private'):
1975 return 'CL %s is private' % self.GetIssue()
1976 return None
1977
tandrii8c5a3532016-11-04 07:52:02 -07001978 def GetTryjobProperties(self, patchset=None):
1979 """Returns dictionary of properties to launch tryjob."""
1980 project = (self.GetIssueProperties() or {}).get('project')
1981 return {
1982 'issue': self.GetIssue(),
1983 'patch_project': project,
1984 'patch_storage': 'rietveld',
1985 'patchset': patchset or self.GetPatchset(),
1986 'rietveld': self.GetCodereviewServer(),
1987 }
1988
tandriide281ae2016-10-12 06:02:30 -07001989 def GetIssueOwner(self):
1990 return (self.GetIssueProperties() or {}).get('owner_email')
1991
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001992 def AddComment(self, message):
1993 return self.RpcServer().add_comment(self.GetIssue(), message)
1994
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001995 def GetCommentsSummary(self):
1996 summary = []
1997 for message in self.GetIssueProperties().get('messages', []):
1998 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
1999 summary.append(_CommentSummary(
2000 date=date,
2001 disapproval=bool(message['disapproval']),
2002 approval=bool(message['approval']),
2003 sender=message['sender'],
2004 message=message['text'],
2005 ))
2006 return summary
2007
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002008 def GetStatus(self):
2009 """Apply a rough heuristic to give a simple summary of an issue's review
2010 or CQ status, assuming adherence to a common workflow.
2011
2012 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002013 * 'error' - error from review tool (including deleted issues)
2014 * 'unsent' - not sent for review
2015 * 'waiting' - waiting for review
2016 * 'reply' - waiting for owner to reply to review
2017 * 'not lgtm' - Code-Review label has been set negatively
2018 * 'lgtm' - LGTM from at least one approved reviewer
2019 * 'commit' - in the commit queue
2020 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002021 """
2022 if not self.GetIssue():
2023 return None
2024
2025 try:
2026 props = self.GetIssueProperties()
2027 except urllib2.HTTPError:
2028 return 'error'
2029
2030 if props.get('closed'):
2031 # Issue is closed.
2032 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002033 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002034 # Issue is in the commit queue.
2035 return 'commit'
2036
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002037 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002038 if not messages:
2039 # No message was sent.
2040 return 'unsent'
2041
2042 if get_approving_reviewers(props):
2043 return 'lgtm'
2044 elif get_approving_reviewers(props, disapproval=True):
2045 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002046
tandrii9d2c7a32016-06-22 03:42:45 -07002047 # Skip CQ messages that don't require owner's action.
2048 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2049 if 'Dry run:' in messages[-1]['text']:
2050 messages.pop()
2051 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2052 # This message always follows prior messages from CQ,
2053 # so skip this too.
2054 messages.pop()
2055 else:
2056 # This is probably a CQ messages warranting user attention.
2057 break
2058
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002059 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002060 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002061 return 'reply'
2062 return 'waiting'
2063
dsansomee2d6fd92016-09-08 00:10:47 -07002064 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002065 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002066
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002067 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002068 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002069
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002070 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002071 return self.SetFlags({flag: value})
2072
2073 def SetFlags(self, flags):
2074 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002075 """
phajdan.jr68598232016-08-10 03:28:28 -07002076 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002077 try:
tandrii4b233bd2016-07-06 03:50:29 -07002078 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002079 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002080 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002081 if e.code == 404:
2082 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2083 if e.code == 403:
2084 DieWithError(
2085 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002086 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002087 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002088
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002089 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002090 """Returns an upload.RpcServer() to access this review's rietveld instance.
2091 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002092 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002093 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002094 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002095 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002096 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002097
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002098 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002099 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002100 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002101
tandrii5d48c322016-08-18 16:19:37 -07002102 @classmethod
2103 def PatchsetConfigKey(cls):
2104 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002105
tandrii5d48c322016-08-18 16:19:37 -07002106 @classmethod
2107 def CodereviewServerConfigKey(cls):
2108 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002109
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002110 def GetRieveldObjForPresubmit(self):
2111 return self.RpcServer()
2112
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002113 def SetCQState(self, new_state):
2114 props = self.GetIssueProperties()
2115 if props.get('private'):
2116 DieWithError('Cannot set-commit on private issue')
2117
2118 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002119 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002120 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002121 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002122 else:
tandrii4b233bd2016-07-06 03:50:29 -07002123 assert new_state == _CQState.DRY_RUN
2124 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002125
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002126 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2127 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002128 # PatchIssue should never be called with a dirty tree. It is up to the
2129 # caller to check this, but just in case we assert here since the
2130 # consequences of the caller not checking this could be dire.
2131 assert(not git_common.is_dirty_git_tree('apply'))
2132 assert(parsed_issue_arg.valid)
2133 self._changelist.issue = parsed_issue_arg.issue
2134 if parsed_issue_arg.hostname:
2135 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2136
skobes6468b902016-10-24 08:45:10 -07002137 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2138 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2139 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002140 try:
skobes6468b902016-10-24 08:45:10 -07002141 scm_obj.apply_patch(patchset_object)
2142 except Exception as e:
2143 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002144 return 1
2145
2146 # If we had an issue, commit the current state and register the issue.
2147 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002148 self.SetIssue(self.GetIssue())
2149 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002150 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2151 'patch from issue %(i)s at patchset '
2152 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2153 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002154 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002155 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002156 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002157 return 0
2158
2159 @staticmethod
2160 def ParseIssueURL(parsed_url):
2161 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2162 return None
wychen3c1c1722016-08-04 11:46:36 -07002163 # Rietveld patch: https://domain/<number>/#ps<patchset>
2164 match = re.match(r'/(\d+)/$', parsed_url.path)
2165 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2166 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002167 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002168 issue=int(match.group(1)),
2169 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002170 hostname=parsed_url.netloc,
2171 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002172 # Typical url: https://domain/<issue_number>[/[other]]
2173 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2174 if match:
skobes6468b902016-10-24 08:45:10 -07002175 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002176 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002177 hostname=parsed_url.netloc,
2178 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002179 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2180 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2181 if match:
skobes6468b902016-10-24 08:45:10 -07002182 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002183 issue=int(match.group(1)),
2184 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002185 hostname=parsed_url.netloc,
2186 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002187 return None
2188
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002189 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002190 """Upload the patch to Rietveld."""
2191 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2192 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002193 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2194 if options.emulate_svn_auto_props:
2195 upload_args.append('--emulate_svn_auto_props')
2196
2197 change_desc = None
2198
2199 if options.email is not None:
2200 upload_args.extend(['--email', options.email])
2201
2202 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002203 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002204 upload_args.extend(['--title', options.title])
2205 if options.message:
2206 upload_args.extend(['--message', options.message])
2207 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002208 print('This branch is associated with issue %s. '
2209 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002210 else:
nodirca166002016-06-27 10:59:51 -07002211 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002212 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002213 if options.message:
2214 message = options.message
2215 else:
2216 message = CreateDescriptionFromLog(args)
2217 if options.title:
2218 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002219 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002220 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002221 change_desc.update_reviewers(options.reviewers, options.tbrs,
2222 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002223 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002224 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002225
2226 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002227 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002228 return 1
2229
2230 upload_args.extend(['--message', change_desc.description])
2231 if change_desc.get_reviewers():
2232 upload_args.append('--reviewers=%s' % ','.join(
2233 change_desc.get_reviewers()))
2234 if options.send_mail:
2235 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002236 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002237 upload_args.append('--send_mail')
2238
2239 # We check this before applying rietveld.private assuming that in
2240 # rietveld.cc only addresses which we can send private CLs to are listed
2241 # if rietveld.private is set, and so we should ignore rietveld.cc only
2242 # when --private is specified explicitly on the command line.
2243 if options.private:
2244 logging.warn('rietveld.cc is ignored since private flag is specified. '
2245 'You need to review and add them manually if necessary.')
2246 cc = self.GetCCListWithoutDefault()
2247 else:
2248 cc = self.GetCCList()
2249 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002250 if change_desc.get_cced():
2251 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002252 if cc:
2253 upload_args.extend(['--cc', cc])
2254
2255 if options.private or settings.GetDefaultPrivateFlag() == "True":
2256 upload_args.append('--private')
2257
2258 upload_args.extend(['--git_similarity', str(options.similarity)])
2259 if not options.find_copies:
2260 upload_args.extend(['--git_no_find_copies'])
2261
2262 # Include the upstream repo's URL in the change -- this is useful for
2263 # projects that have their source spread across multiple repos.
2264 remote_url = self.GetGitBaseUrlFromConfig()
2265 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002266 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2267 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2268 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002269 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002270 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002271 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002272 if target_ref:
2273 upload_args.extend(['--target_ref', target_ref])
2274
2275 # Look for dependent patchsets. See crbug.com/480453 for more details.
2276 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2277 upstream_branch = ShortBranchName(upstream_branch)
2278 if remote is '.':
2279 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002280 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002281 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002282 print()
2283 print('Skipping dependency patchset upload because git config '
2284 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2285 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002286 else:
2287 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002288 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002289 auth_config=auth_config)
2290 branch_cl_issue_url = branch_cl.GetIssueURL()
2291 branch_cl_issue = branch_cl.GetIssue()
2292 branch_cl_patchset = branch_cl.GetPatchset()
2293 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2294 upload_args.extend(
2295 ['--depends_on_patchset', '%s:%s' % (
2296 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002297 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002298 '\n'
2299 'The current branch (%s) is tracking a local branch (%s) with '
2300 'an associated CL.\n'
2301 'Adding %s/#ps%s as a dependency patchset.\n'
2302 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2303 branch_cl_patchset))
2304
2305 project = settings.GetProject()
2306 if project:
2307 upload_args.extend(['--project', project])
2308
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002309 try:
2310 upload_args = ['upload'] + upload_args + args
2311 logging.info('upload.RealMain(%s)', upload_args)
2312 issue, patchset = upload.RealMain(upload_args)
2313 issue = int(issue)
2314 patchset = int(patchset)
2315 except KeyboardInterrupt:
2316 sys.exit(1)
2317 except:
2318 # If we got an exception after the user typed a description for their
2319 # change, back up the description before re-raising.
2320 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002321 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002322 raise
2323
2324 if not self.GetIssue():
2325 self.SetIssue(issue)
2326 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002327 return 0
2328
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002329
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002330class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002331 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002332 # auth_config is Rietveld thing, kept here to preserve interface only.
2333 super(_GerritChangelistImpl, self).__init__(changelist)
2334 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002335 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002336 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002337 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002338 # Map from change number (issue) to its detail cache.
2339 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002340
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002341 if codereview_host is not None:
2342 assert not codereview_host.startswith('https://'), codereview_host
2343 self._gerrit_host = codereview_host
2344 self._gerrit_server = 'https://%s' % codereview_host
2345
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002346 def _GetGerritHost(self):
2347 # Lazy load of configs.
2348 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002349 if self._gerrit_host and '.' not in self._gerrit_host:
2350 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2351 # This happens for internal stuff http://crbug.com/614312.
2352 parsed = urlparse.urlparse(self.GetRemoteUrl())
2353 if parsed.scheme == 'sso':
2354 print('WARNING: using non https URLs for remote is likely broken\n'
2355 ' Your current remote is: %s' % self.GetRemoteUrl())
2356 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2357 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002358 return self._gerrit_host
2359
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002360 def _GetGitHost(self):
2361 """Returns git host to be used when uploading change to Gerrit."""
2362 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2363
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002364 def GetCodereviewServer(self):
2365 if not self._gerrit_server:
2366 # If we're on a branch then get the server potentially associated
2367 # with that branch.
2368 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002369 self._gerrit_server = self._GitGetBranchConfigValue(
2370 self.CodereviewServerConfigKey())
2371 if self._gerrit_server:
2372 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002373 if not self._gerrit_server:
2374 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2375 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002376 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002377 parts[0] = parts[0] + '-review'
2378 self._gerrit_host = '.'.join(parts)
2379 self._gerrit_server = 'https://%s' % self._gerrit_host
2380 return self._gerrit_server
2381
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002382 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002383 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002384 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002385
tandrii5d48c322016-08-18 16:19:37 -07002386 @classmethod
2387 def PatchsetConfigKey(cls):
2388 return 'gerritpatchset'
2389
2390 @classmethod
2391 def CodereviewServerConfigKey(cls):
2392 return 'gerritserver'
2393
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002394 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002395 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002396 if settings.GetGerritSkipEnsureAuthenticated():
2397 # For projects with unusual authentication schemes.
2398 # See http://crbug.com/603378.
2399 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002400 # Lazy-loader to identify Gerrit and Git hosts.
2401 if gerrit_util.GceAuthenticator.is_gce():
2402 return
2403 self.GetCodereviewServer()
2404 git_host = self._GetGitHost()
2405 assert self._gerrit_server and self._gerrit_host
2406 cookie_auth = gerrit_util.CookiesAuthenticator()
2407
2408 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2409 git_auth = cookie_auth.get_auth_header(git_host)
2410 if gerrit_auth and git_auth:
2411 if gerrit_auth == git_auth:
2412 return
2413 print((
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002414 'WARNING: you have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002415 ' %s\n'
2416 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002417 ' Consider running the following command:\n'
2418 ' git cl creds-check\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002419 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002420 (git_host, self._gerrit_host,
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002421 cookie_auth.get_new_password_message(git_host)))
2422 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002423 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002424 return
2425 else:
2426 missing = (
2427 [] if gerrit_auth else [self._gerrit_host] +
2428 [] if git_auth else [git_host])
2429 DieWithError('Credentials for the following hosts are required:\n'
2430 ' %s\n'
2431 'These are read from %s (or legacy %s)\n'
2432 '%s' % (
2433 '\n '.join(missing),
2434 cookie_auth.get_gitcookies_path(),
2435 cookie_auth.get_netrc_path(),
2436 cookie_auth.get_new_password_message(git_host)))
2437
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002438 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002439 if not self.GetIssue():
2440 return
2441
2442 # Warm change details cache now to avoid RPCs later, reducing latency for
2443 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002444 self._GetChangeDetail(
2445 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002446
2447 status = self._GetChangeDetail()['status']
2448 if status in ('MERGED', 'ABANDONED'):
2449 DieWithError('Change %s has been %s, new uploads are not allowed' %
2450 (self.GetIssueURL(),
2451 'submitted' if status == 'MERGED' else 'abandoned'))
2452
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002453 if gerrit_util.GceAuthenticator.is_gce():
2454 return
2455 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2456 self._GetGerritHost())
2457 if self.GetIssueOwner() == cookies_user:
2458 return
2459 logging.debug('change %s owner is %s, cookies user is %s',
2460 self.GetIssue(), self.GetIssueOwner(), cookies_user)
2461 # Maybe user has linked accounts or smth like that,
2462 # so ask what Gerrit thinks of this user.
2463 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2464 if details['email'] == self.GetIssueOwner():
2465 return
2466 if not force:
2467 print('WARNING: change %s is owned by %s, but you authenticate to Gerrit '
2468 'as %s.\n'
2469 'Uploading may fail due to lack of permissions.' %
2470 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2471 confirm_or_exit(action='upload')
2472
2473
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002474 def _PostUnsetIssueProperties(self):
2475 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002476 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002477
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002478 def GetRieveldObjForPresubmit(self):
2479 class ThisIsNotRietveldIssue(object):
2480 def __nonzero__(self):
2481 # This is a hack to make presubmit_support think that rietveld is not
2482 # defined, yet still ensure that calls directly result in a decent
2483 # exception message below.
2484 return False
2485
2486 def __getattr__(self, attr):
2487 print(
2488 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2489 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2490 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2491 'or use Rietveld for codereview.\n'
2492 'See also http://crbug.com/579160.' % attr)
2493 raise NotImplementedError()
2494 return ThisIsNotRietveldIssue()
2495
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002496 def GetGerritObjForPresubmit(self):
2497 return presubmit_support.GerritAccessor(self._GetGerritHost())
2498
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002499 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002500 """Apply a rough heuristic to give a simple summary of an issue's review
2501 or CQ status, assuming adherence to a common workflow.
2502
2503 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002504 * 'error' - error from review tool (including deleted issues)
2505 * 'unsent' - no reviewers added
2506 * 'waiting' - waiting for review
2507 * 'reply' - waiting for uploader to reply to review
2508 * 'lgtm' - Code-Review label has been set
2509 * 'commit' - in the commit queue
2510 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002511 """
2512 if not self.GetIssue():
2513 return None
2514
2515 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002516 data = self._GetChangeDetail([
2517 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002518 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002519 return 'error'
2520
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002521 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002522 return 'closed'
2523
Aaron Gable9ab38c62017-04-06 14:36:33 -07002524 if data['labels'].get('Commit-Queue', {}).get('approved'):
2525 # The section will have an "approved" subsection if anyone has voted
2526 # the maximum value on the label.
2527 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002528
Aaron Gable9ab38c62017-04-06 14:36:33 -07002529 if data['labels'].get('Code-Review', {}).get('approved'):
2530 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002531
2532 if not data.get('reviewers', {}).get('REVIEWER', []):
2533 return 'unsent'
2534
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002535 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002536 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2537 last_message_author = messages.pop().get('author', {})
2538 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002539 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2540 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002541 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002542 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002543 if last_message_author.get('_account_id') == owner:
2544 # Most recent message was by owner.
2545 return 'waiting'
2546 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002547 # Some reply from non-owner.
2548 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002549
2550 # Somehow there are no messages even though there are reviewers.
2551 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002552
2553 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002554 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002555 return data['revisions'][data['current_revision']]['_number']
2556
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002557 def FetchDescription(self, force=False):
2558 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2559 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002560 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002561 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002562
dsansomee2d6fd92016-09-08 00:10:47 -07002563 def UpdateDescriptionRemote(self, description, force=False):
2564 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2565 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002566 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002567 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002568 'unpublished edit. Either publish the edit in the Gerrit web UI '
2569 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002570
2571 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2572 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002573 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002574 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002575
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002576 def AddComment(self, message):
2577 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2578 msg=message)
2579
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002580 def GetCommentsSummary(self):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002581 # DETAILED_ACCOUNTS is to get emails in accounts.
2582 data = self._GetChangeDetail(options=['MESSAGES', 'DETAILED_ACCOUNTS'])
2583 summary = []
2584 for msg in data.get('messages', []):
2585 # Gerrit spits out nanoseconds.
2586 assert len(msg['date'].split('.')[-1]) == 9
2587 date = datetime.datetime.strptime(msg['date'][:-3],
2588 '%Y-%m-%d %H:%M:%S.%f')
2589 summary.append(_CommentSummary(
2590 date=date,
2591 message=msg['message'],
2592 sender=msg['author']['email'],
2593 # These could be inferred from the text messages and correlated with
2594 # Code-Review label maximum, however this is not reliable.
2595 # Leaving as is until the need arises.
2596 approval=False,
2597 disapproval=False,
2598 ))
2599 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002600
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002601 def CloseIssue(self):
2602 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2603
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002604 def SubmitIssue(self, wait_for_merge=True):
2605 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2606 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002607
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002608 def _GetChangeDetail(self, options=None, issue=None,
2609 no_cache=False):
2610 """Returns details of the issue by querying Gerrit and caching results.
2611
2612 If fresh data is needed, set no_cache=True which will clear cache and
2613 thus new data will be fetched from Gerrit.
2614 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002615 options = options or []
2616 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002617 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002618
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002619 # Optimization to avoid multiple RPCs:
2620 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2621 'CURRENT_COMMIT' not in options):
2622 options.append('CURRENT_COMMIT')
2623
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002624 # Normalize issue and options for consistent keys in cache.
2625 issue = str(issue)
2626 options = [o.upper() for o in options]
2627
2628 # Check in cache first unless no_cache is True.
2629 if no_cache:
2630 self._detail_cache.pop(issue, None)
2631 else:
2632 options_set = frozenset(options)
2633 for cached_options_set, data in self._detail_cache.get(issue, []):
2634 # Assumption: data fetched before with extra options is suitable
2635 # for return for a smaller set of options.
2636 # For example, if we cached data for
2637 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2638 # and request is for options=[CURRENT_REVISION],
2639 # THEN we can return prior cached data.
2640 if options_set.issubset(cached_options_set):
2641 return data
2642
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002643 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002644 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002645 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002646 except gerrit_util.GerritError as e:
2647 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002648 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002649 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002650
2651 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002652 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002653
agable32978d92016-11-01 12:55:02 -07002654 def _GetChangeCommit(self, issue=None):
2655 issue = issue or self.GetIssue()
2656 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002657 try:
2658 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2659 except gerrit_util.GerritError as e:
2660 if e.http_status == 404:
2661 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2662 raise
agable32978d92016-11-01 12:55:02 -07002663 return data
2664
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002665 def CMDLand(self, force, bypass_hooks, verbose):
2666 if git_common.is_dirty_git_tree('land'):
2667 return 1
tandriid60367b2016-06-22 05:25:12 -07002668 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2669 if u'Commit-Queue' in detail.get('labels', {}):
2670 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002671 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2672 'which can test and land changes for you. '
2673 'Are you sure you wish to bypass it?\n',
2674 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002675
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002676 differs = True
tandriic4344b52016-08-29 06:04:54 -07002677 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002678 # Note: git diff outputs nothing if there is no diff.
2679 if not last_upload or RunGit(['diff', last_upload]).strip():
2680 print('WARNING: some changes from local branch haven\'t been uploaded')
2681 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002682 if detail['current_revision'] == last_upload:
2683 differs = False
2684 else:
2685 print('WARNING: local branch contents differ from latest uploaded '
2686 'patchset')
2687 if differs:
2688 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002689 confirm_or_exit(
2690 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2691 action='submit')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002692 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2693 elif not bypass_hooks:
2694 hook_results = self.RunHook(
2695 committing=True,
2696 may_prompt=not force,
2697 verbose=verbose,
2698 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2699 if not hook_results.should_continue():
2700 return 1
2701
2702 self.SubmitIssue(wait_for_merge=True)
2703 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002704 links = self._GetChangeCommit().get('web_links', [])
2705 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002706 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002707 print('Landed as %s' % link.get('url'))
2708 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002709 return 0
2710
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002711 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2712 directory):
2713 assert not reject
2714 assert not nocommit
2715 assert not directory
2716 assert parsed_issue_arg.valid
2717
2718 self._changelist.issue = parsed_issue_arg.issue
2719
2720 if parsed_issue_arg.hostname:
2721 self._gerrit_host = parsed_issue_arg.hostname
2722 self._gerrit_server = 'https://%s' % self._gerrit_host
2723
tandriic2405f52016-10-10 08:13:15 -07002724 try:
2725 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002726 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002727 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002728
2729 if not parsed_issue_arg.patchset:
2730 # Use current revision by default.
2731 revision_info = detail['revisions'][detail['current_revision']]
2732 patchset = int(revision_info['_number'])
2733 else:
2734 patchset = parsed_issue_arg.patchset
2735 for revision_info in detail['revisions'].itervalues():
2736 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2737 break
2738 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002739 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002740 (parsed_issue_arg.patchset, self.GetIssue()))
2741
2742 fetch_info = revision_info['fetch']['http']
2743 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002744 self.SetIssue(self.GetIssue())
2745 self.SetPatchset(patchset)
Aaron Gabled343c632017-03-15 11:02:26 -07002746 RunGit(['cherry-pick', 'FETCH_HEAD'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002747 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002748 (self.GetIssue(), self.GetPatchset()))
2749 return 0
2750
2751 @staticmethod
2752 def ParseIssueURL(parsed_url):
2753 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2754 return None
2755 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2756 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2757 # Short urls like https://domain/<issue_number> can be used, but don't allow
2758 # specifying the patchset (you'd 404), but we allow that here.
2759 if parsed_url.path == '/':
2760 part = parsed_url.fragment
2761 else:
2762 part = parsed_url.path
2763 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2764 if match:
2765 return _ParsedIssueNumberArgument(
2766 issue=int(match.group(2)),
2767 patchset=int(match.group(4)) if match.group(4) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002768 hostname=parsed_url.netloc,
2769 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002770 return None
2771
tandrii16e0b4e2016-06-07 10:34:28 -07002772 def _GerritCommitMsgHookCheck(self, offer_removal):
2773 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2774 if not os.path.exists(hook):
2775 return
2776 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2777 # custom developer made one.
2778 data = gclient_utils.FileRead(hook)
2779 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2780 return
2781 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002782 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002783 'and may interfere with it in subtle ways.\n'
2784 'We recommend you remove the commit-msg hook.')
2785 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002786 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002787 gclient_utils.rm_file_or_tree(hook)
2788 print('Gerrit commit-msg hook removed.')
2789 else:
2790 print('OK, will keep Gerrit commit-msg hook in place.')
2791
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002792 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002793 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002794 if options.squash and options.no_squash:
2795 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002796
2797 if not options.squash and not options.no_squash:
2798 # Load default for user, repo, squash=true, in this order.
2799 options.squash = settings.GetSquashGerritUploads()
2800 elif options.no_squash:
2801 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002802
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002803 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002804 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002805
Aaron Gableb56ad332017-01-06 15:24:31 -08002806 # This may be None; default fallback value is determined in logic below.
2807 title = options.title
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002808 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002809
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002810 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002811 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002812 if self.GetIssue():
2813 # Try to get the message from a previous upload.
2814 message = self.GetDescription()
2815 if not message:
2816 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002817 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002818 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002819 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002820 if options.message:
2821 # For compatibility with Rietveld, if -m|--message is given on
2822 # command line, title should be the first line of that message,
2823 # which shouldn't be confused with CL description.
2824 default_title = options.message.strip().split()[0]
2825 else:
2826 default_title = RunGit(
2827 ['show', '-s', '--format=%s', 'HEAD']).strip()
Andrii Shyshkalove00a29b2017-04-10 14:48:28 +02002828 if options.force:
2829 title = default_title
2830 else:
2831 title = ask_for_data(
2832 'Title for patchset [%s]: ' % default_title) or default_title
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002833 if title == default_title:
2834 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002835 change_id = self._GetChangeDetail()['change_id']
2836 while True:
2837 footer_change_ids = git_footers.get_footer_change_id(message)
2838 if footer_change_ids == [change_id]:
2839 break
2840 if not footer_change_ids:
2841 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002842 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002843 continue
2844 # There is already a valid footer but with different or several ids.
2845 # Doing this automatically is non-trivial as we don't want to lose
2846 # existing other footers, yet we want to append just 1 desired
2847 # Change-Id. Thus, just create a new footer, but let user verify the
2848 # new description.
2849 message = '%s\n\nChange-Id: %s' % (message, change_id)
2850 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002851 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002852 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002853 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002854 'Please, check the proposed correction to the description, '
2855 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2856 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2857 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002858 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002859 if not options.force:
2860 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002861 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002862 message = change_desc.description
2863 if not message:
2864 DieWithError("Description is empty. Aborting...")
2865 # Continue the while loop.
2866 # Sanity check of this code - we should end up with proper message
2867 # footer.
2868 assert [change_id] == git_footers.get_footer_change_id(message)
2869 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002870 else: # if not self.GetIssue()
2871 if options.message:
2872 message = options.message
2873 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002874 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002875 if options.title:
2876 message = options.title + '\n\n' + message
2877 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002878
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002879 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002880 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002881 # On first upload, patchset title is always this string, while
2882 # --title flag gets converted to first line of message.
2883 title = 'Initial upload'
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002884 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002885 if not change_desc.description:
2886 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002887 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002888 if len(change_ids) > 1:
2889 DieWithError('too many Change-Id footers, at most 1 allowed.')
2890 if not change_ids:
2891 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002892 change_desc.set_description(git_footers.add_footer_change_id(
2893 change_desc.description,
2894 GenerateGerritChangeId(change_desc.description)))
2895 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002896 assert len(change_ids) == 1
2897 change_id = change_ids[0]
2898
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002899 if options.reviewers or options.tbrs or options.add_owners_to:
2900 change_desc.update_reviewers(options.reviewers, options.tbrs,
2901 options.add_owners_to, change)
2902
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002903 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002904 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2905 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002906 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2907 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002908 '-m', change_desc.description]).strip()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002909 else:
2910 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002911 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002912 if not change_desc.description:
2913 DieWithError("Description is empty. Aborting...")
2914
2915 if not git_footers.get_footer_change_id(change_desc.description):
2916 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002917 change_desc.set_description(
2918 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002919 if options.reviewers or options.tbrs or options.add_owners_to:
2920 change_desc.update_reviewers(options.reviewers, options.tbrs,
2921 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002922 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002923 # For no-squash mode, we assume the remote called "origin" is the one we
2924 # want. It is not worthwhile to support different workflows for
2925 # no-squash mode.
2926 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002927 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2928
2929 assert change_desc
2930 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2931 ref_to_push)]).splitlines()
2932 if len(commits) > 1:
2933 print('WARNING: This will upload %d commits. Run the following command '
2934 'to see which commits will be uploaded: ' % len(commits))
2935 print('git log %s..%s' % (parent, ref_to_push))
2936 print('You can also use `git squash-branch` to squash these into a '
2937 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002938 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002939
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002940 if options.reviewers or options.tbrs or options.add_owners_to:
2941 change_desc.update_reviewers(options.reviewers, options.tbrs,
2942 options.add_owners_to, change)
2943
2944 if options.send_mail:
2945 if not change_desc.get_reviewers():
2946 DieWithError('Must specify reviewers to send email.', change_desc)
2947
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002948 # Extra options that can be specified at push time. Doc:
2949 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002950 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002951 if change_desc.get_reviewers(tbr_only=True):
2952 print('Adding self-LGTM (Code-Review +1) because of TBRs')
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002953 refspec_opts.append('l=Code-Review+1')
tandrii99a72f22016-08-17 14:33:24 -07002954
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002955
2956 # TODO(tandrii): options.message should be posted as a comment
2957 # if --send-email is set on non-initial upload as Rietveld used to do it.
2958
Aaron Gable9b713dd2016-12-14 16:04:21 -08002959 if title:
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002960 if not re.match(r'^[\w ]+$', title):
2961 title = re.sub(r'[^\w ]', '', title)
2962 if not automatic_title:
2963 print('WARNING: Patchset title may only contain alphanumeric chars '
2964 'and spaces. You can edit it in the UI. '
2965 'See https://crbug.com/663787.\n'
2966 'Cleaned up title: %s' % title)
2967 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2968 # reverse on its side.
2969 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002970
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002971 # Never notify now because no one is on the review. Notify when we add
2972 # reviewers and CCs below.
2973 refspec_opts.append('notify=NONE')
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002974
agablec6787972016-09-09 16:13:34 -07002975 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002976 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002977
rmistry9eadede2016-09-19 11:22:43 -07002978 if options.topic:
2979 # Documentation on Gerrit topics is here:
2980 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002981 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002982
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002983 refspec_suffix = ''
2984 if refspec_opts:
2985 refspec_suffix = '%' + ','.join(refspec_opts)
2986 assert ' ' not in refspec_suffix, (
2987 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2988 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2989
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002990 try:
2991 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002992 ['git', 'push', self.GetRemoteUrl(), refspec],
2993 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002994 # Flush after every line: useful for seeing progress when running as
2995 # recipe.
2996 filter_fn=lambda _: sys.stdout.flush())
2997 except subprocess2.CalledProcessError:
2998 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002999 'for the reason of the failure.\n'
3000 'Hint: run command below to diangose common Git/Gerrit '
3001 'credential problems:\n'
3002 ' git cl creds-check\n',
3003 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003004
3005 if options.squash:
3006 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
3007 change_numbers = [m.group(1)
3008 for m in map(regex.match, push_stdout.splitlines())
3009 if m]
3010 if len(change_numbers) != 1:
3011 DieWithError(
3012 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003013 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003014 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003015 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003016
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003017 reviewers = sorted(change_desc.get_reviewers())
3018
tandrii88189772016-09-29 04:29:57 -07003019 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003020 if not options.private:
3021 cc = self.GetCCList().split(',')
3022 else:
3023 cc = []
tandrii88189772016-09-29 04:29:57 -07003024 if options.cc:
3025 cc.extend(options.cc)
3026 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003027 if change_desc.get_cced():
3028 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003029
3030 gerrit_util.AddReviewers(
3031 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3032 notify=bool(options.send_mail))
3033
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003034 return 0
3035
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003036 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3037 change_desc):
3038 """Computes parent of the generated commit to be uploaded to Gerrit.
3039
3040 Returns revision or a ref name.
3041 """
3042 if custom_cl_base:
3043 # Try to avoid creating additional unintended CLs when uploading, unless
3044 # user wants to take this risk.
3045 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3046 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3047 local_ref_of_target_remote])
3048 if code == 1:
3049 print('\nWARNING: manually specified base of this CL `%s` '
3050 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3051 'If you proceed with upload, more than 1 CL may be created by '
3052 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3053 'If you are certain that specified base `%s` has already been '
3054 'uploaded to Gerrit as another CL, you may proceed.\n' %
3055 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3056 if not force:
3057 confirm_or_exit(
3058 'Do you take responsibility for cleaning up potential mess '
3059 'resulting from proceeding with upload?',
3060 action='upload')
3061 return custom_cl_base
3062
Aaron Gablef97e33d2017-03-30 15:44:27 -07003063 if remote != '.':
3064 return self.GetCommonAncestorWithUpstream()
3065
3066 # If our upstream branch is local, we base our squashed commit on its
3067 # squashed version.
3068 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3069
Aaron Gablef97e33d2017-03-30 15:44:27 -07003070 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003071 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003072
3073 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003074 # TODO(tandrii): consider checking parent change in Gerrit and using its
3075 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3076 # the tree hash of the parent branch. The upside is less likely bogus
3077 # requests to reupload parent change just because it's uploadhash is
3078 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003079 parent = RunGit(['config',
3080 'branch.%s.gerritsquashhash' % upstream_branch_name],
3081 error_ok=True).strip()
3082 # Verify that the upstream branch has been uploaded too, otherwise
3083 # Gerrit will create additional CLs when uploading.
3084 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3085 RunGitSilent(['rev-parse', parent + ':'])):
3086 DieWithError(
3087 '\nUpload upstream branch %s first.\n'
3088 'It is likely that this branch has been rebased since its last '
3089 'upload, so you just need to upload it again.\n'
3090 '(If you uploaded it with --no-squash, then branch dependencies '
3091 'are not supported, and you should reupload with --squash.)'
3092 % upstream_branch_name,
3093 change_desc)
3094 return parent
3095
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003096 def _AddChangeIdToCommitMessage(self, options, args):
3097 """Re-commits using the current message, assumes the commit hook is in
3098 place.
3099 """
3100 log_desc = options.message or CreateDescriptionFromLog(args)
3101 git_command = ['commit', '--amend', '-m', log_desc]
3102 RunGit(git_command)
3103 new_log_desc = CreateDescriptionFromLog(args)
3104 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003105 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003106 return new_log_desc
3107 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003108 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003109
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003110 def SetCQState(self, new_state):
3111 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003112 vote_map = {
3113 _CQState.NONE: 0,
3114 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003115 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003116 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01003117 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
3118 if new_state == _CQState.DRY_RUN:
3119 # Don't spam everybody reviewer/owner.
3120 kwargs['notify'] = 'NONE'
3121 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003122
tandriie113dfd2016-10-11 10:20:12 -07003123 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003124 try:
3125 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003126 except GerritChangeNotExists:
3127 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003128
3129 if data['status'] in ('ABANDONED', 'MERGED'):
3130 return 'CL %s is closed' % self.GetIssue()
3131
3132 def GetTryjobProperties(self, patchset=None):
3133 """Returns dictionary of properties to launch tryjob."""
3134 data = self._GetChangeDetail(['ALL_REVISIONS'])
3135 patchset = int(patchset or self.GetPatchset())
3136 assert patchset
3137 revision_data = None # Pylint wants it to be defined.
3138 for revision_data in data['revisions'].itervalues():
3139 if int(revision_data['_number']) == patchset:
3140 break
3141 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003142 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003143 (patchset, self.GetIssue()))
3144 return {
3145 'patch_issue': self.GetIssue(),
3146 'patch_set': patchset or self.GetPatchset(),
3147 'patch_project': data['project'],
3148 'patch_storage': 'gerrit',
3149 'patch_ref': revision_data['fetch']['http']['ref'],
3150 'patch_repository_url': revision_data['fetch']['http']['url'],
3151 'patch_gerrit_url': self.GetCodereviewServer(),
3152 }
tandriie113dfd2016-10-11 10:20:12 -07003153
tandriide281ae2016-10-12 06:02:30 -07003154 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003155 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003156
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003157
3158_CODEREVIEW_IMPLEMENTATIONS = {
3159 'rietveld': _RietveldChangelistImpl,
3160 'gerrit': _GerritChangelistImpl,
3161}
3162
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003163
iannuccie53c9352016-08-17 14:40:40 -07003164def _add_codereview_issue_select_options(parser, extra=""):
3165 _add_codereview_select_options(parser)
3166
3167 text = ('Operate on this issue number instead of the current branch\'s '
3168 'implicit issue.')
3169 if extra:
3170 text += ' '+extra
3171 parser.add_option('-i', '--issue', type=int, help=text)
3172
3173
3174def _process_codereview_issue_select_options(parser, options):
3175 _process_codereview_select_options(parser, options)
3176 if options.issue is not None and not options.forced_codereview:
3177 parser.error('--issue must be specified with either --rietveld or --gerrit')
3178
3179
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003180def _add_codereview_select_options(parser):
3181 """Appends --gerrit and --rietveld options to force specific codereview."""
3182 parser.codereview_group = optparse.OptionGroup(
3183 parser, 'EXPERIMENTAL! Codereview override options')
3184 parser.add_option_group(parser.codereview_group)
3185 parser.codereview_group.add_option(
3186 '--gerrit', action='store_true',
3187 help='Force the use of Gerrit for codereview')
3188 parser.codereview_group.add_option(
3189 '--rietveld', action='store_true',
3190 help='Force the use of Rietveld for codereview')
3191
3192
3193def _process_codereview_select_options(parser, options):
3194 if options.gerrit and options.rietveld:
3195 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3196 options.forced_codereview = None
3197 if options.gerrit:
3198 options.forced_codereview = 'gerrit'
3199 elif options.rietveld:
3200 options.forced_codereview = 'rietveld'
3201
3202
tandriif9aefb72016-07-01 09:06:51 -07003203def _get_bug_line_values(default_project, bugs):
3204 """Given default_project and comma separated list of bugs, yields bug line
3205 values.
3206
3207 Each bug can be either:
3208 * a number, which is combined with default_project
3209 * string, which is left as is.
3210
3211 This function may produce more than one line, because bugdroid expects one
3212 project per line.
3213
3214 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3215 ['v8:123', 'chromium:789']
3216 """
3217 default_bugs = []
3218 others = []
3219 for bug in bugs.split(','):
3220 bug = bug.strip()
3221 if bug:
3222 try:
3223 default_bugs.append(int(bug))
3224 except ValueError:
3225 others.append(bug)
3226
3227 if default_bugs:
3228 default_bugs = ','.join(map(str, default_bugs))
3229 if default_project:
3230 yield '%s:%s' % (default_project, default_bugs)
3231 else:
3232 yield default_bugs
3233 for other in sorted(others):
3234 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3235 yield other
3236
3237
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003238class ChangeDescription(object):
3239 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003240 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003241 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003242 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003243 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003244
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003245 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003246 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003247
agable@chromium.org42c20792013-09-12 17:34:49 +00003248 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003249 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003250 return '\n'.join(self._description_lines)
3251
3252 def set_description(self, desc):
3253 if isinstance(desc, basestring):
3254 lines = desc.splitlines()
3255 else:
3256 lines = [line.rstrip() for line in desc]
3257 while lines and not lines[0]:
3258 lines.pop(0)
3259 while lines and not lines[-1]:
3260 lines.pop(-1)
3261 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003262
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003263 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3264 """Rewrites the R=/TBR= line(s) as a single line each.
3265
3266 Args:
3267 reviewers (list(str)) - list of additional emails to use for reviewers.
3268 tbrs (list(str)) - list of additional emails to use for TBRs.
3269 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3270 the change that are missing OWNER coverage. If this is not None, you
3271 must also pass a value for `change`.
3272 change (Change) - The Change that should be used for OWNERS lookups.
3273 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003274 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003275 assert isinstance(tbrs, list), tbrs
3276
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003277 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003278 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003279
3280 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003281 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003282
3283 reviewers = set(reviewers)
3284 tbrs = set(tbrs)
3285 LOOKUP = {
3286 'TBR': tbrs,
3287 'R': reviewers,
3288 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003289
agable@chromium.org42c20792013-09-12 17:34:49 +00003290 # Get the set of R= and TBR= lines and remove them from the desciption.
3291 regexp = re.compile(self.R_LINE)
3292 matches = [regexp.match(line) for line in self._description_lines]
3293 new_desc = [l for i, l in enumerate(self._description_lines)
3294 if not matches[i]]
3295 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003296
agable@chromium.org42c20792013-09-12 17:34:49 +00003297 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003298
3299 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003300 for match in matches:
3301 if not match:
3302 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003303 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3304
3305 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003306 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003307 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003308 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003309 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003310 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003311 LOOKUP[add_owners_to].update(
3312 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003313
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003314 # If any folks ended up in both groups, remove them from tbrs.
3315 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003316
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003317 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3318 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003319
3320 # Put the new lines in the description where the old first R= line was.
3321 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3322 if 0 <= line_loc < len(self._description_lines):
3323 if new_tbr_line:
3324 self._description_lines.insert(line_loc, new_tbr_line)
3325 if new_r_line:
3326 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003327 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003328 if new_r_line:
3329 self.append_footer(new_r_line)
3330 if new_tbr_line:
3331 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003332
Aaron Gable3a16ed12017-03-23 10:51:55 -07003333 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003334 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003335 self.set_description([
3336 '# Enter a description of the change.',
3337 '# This will be displayed on the codereview site.',
3338 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003339 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003340 '--------------------',
3341 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003342
agable@chromium.org42c20792013-09-12 17:34:49 +00003343 regexp = re.compile(self.BUG_LINE)
3344 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003345 prefix = settings.GetBugPrefix()
3346 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003347 if git_footer:
3348 self.append_footer('Bug: %s' % ', '.join(values))
3349 else:
3350 for value in values:
3351 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003352
agable@chromium.org42c20792013-09-12 17:34:49 +00003353 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003354 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003355 if not content:
3356 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003357 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003358
3359 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003360 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3361 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003362 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003363 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003364
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003365 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003366 """Adds a footer line to the description.
3367
3368 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3369 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3370 that Gerrit footers are always at the end.
3371 """
3372 parsed_footer_line = git_footers.parse_footer(line)
3373 if parsed_footer_line:
3374 # Line is a gerrit footer in the form: Footer-Key: any value.
3375 # Thus, must be appended observing Gerrit footer rules.
3376 self.set_description(
3377 git_footers.add_footer(self.description,
3378 key=parsed_footer_line[0],
3379 value=parsed_footer_line[1]))
3380 return
3381
3382 if not self._description_lines:
3383 self._description_lines.append(line)
3384 return
3385
3386 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3387 if gerrit_footers:
3388 # git_footers.split_footers ensures that there is an empty line before
3389 # actual (gerrit) footers, if any. We have to keep it that way.
3390 assert top_lines and top_lines[-1] == ''
3391 top_lines, separator = top_lines[:-1], top_lines[-1:]
3392 else:
3393 separator = [] # No need for separator if there are no gerrit_footers.
3394
3395 prev_line = top_lines[-1] if top_lines else ''
3396 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3397 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3398 top_lines.append('')
3399 top_lines.append(line)
3400 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003401
tandrii99a72f22016-08-17 14:33:24 -07003402 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003403 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003404 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003405 reviewers = [match.group(2).strip()
3406 for match in matches
3407 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003408 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003409
bradnelsond975b302016-10-23 12:20:23 -07003410 def get_cced(self):
3411 """Retrieves the list of reviewers."""
3412 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3413 cced = [match.group(2).strip() for match in matches if match]
3414 return cleanup_list(cced)
3415
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003416 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3417 """Updates this commit description given the parent.
3418
3419 This is essentially what Gnumbd used to do.
3420 Consult https://goo.gl/WMmpDe for more details.
3421 """
3422 assert parent_msg # No, orphan branch creation isn't supported.
3423 assert parent_hash
3424 assert dest_ref
3425 parent_footer_map = git_footers.parse_footers(parent_msg)
3426 # This will also happily parse svn-position, which GnumbD is no longer
3427 # supporting. While we'd generate correct footers, the verifier plugin
3428 # installed in Gerrit will block such commit (ie git push below will fail).
3429 parent_position = git_footers.get_position(parent_footer_map)
3430
3431 # Cherry-picks may have last line obscuring their prior footers,
3432 # from git_footers perspective. This is also what Gnumbd did.
3433 cp_line = None
3434 if (self._description_lines and
3435 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3436 cp_line = self._description_lines.pop()
3437
3438 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3439
3440 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3441 # user interference with actual footers we'd insert below.
3442 for i, (k, v) in enumerate(parsed_footers):
3443 if k.startswith('Cr-'):
3444 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3445
3446 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003447 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003448 if parent_position[0] == dest_ref:
3449 # Same branch as parent.
3450 number = int(parent_position[1]) + 1
3451 else:
3452 number = 1 # New branch, and extra lineage.
3453 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3454 int(parent_position[1])))
3455
3456 parsed_footers.append(('Cr-Commit-Position',
3457 '%s@{#%d}' % (dest_ref, number)))
3458 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3459
3460 self._description_lines = top_lines
3461 if cp_line:
3462 self._description_lines.append(cp_line)
3463 if self._description_lines[-1] != '':
3464 self._description_lines.append('') # Ensure footer separator.
3465 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3466
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003467
Aaron Gablea1bab272017-04-11 16:38:18 -07003468def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003469 """Retrieves the reviewers that approved a CL from the issue properties with
3470 messages.
3471
3472 Note that the list may contain reviewers that are not committer, thus are not
3473 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003474
3475 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003476 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003477 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003478 return sorted(
3479 set(
3480 message['sender']
3481 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003482 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003483 )
3484 )
3485
3486
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003487def FindCodereviewSettingsFile(filename='codereview.settings'):
3488 """Finds the given file starting in the cwd and going up.
3489
3490 Only looks up to the top of the repository unless an
3491 'inherit-review-settings-ok' file exists in the root of the repository.
3492 """
3493 inherit_ok_file = 'inherit-review-settings-ok'
3494 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003495 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003496 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3497 root = '/'
3498 while True:
3499 if filename in os.listdir(cwd):
3500 if os.path.isfile(os.path.join(cwd, filename)):
3501 return open(os.path.join(cwd, filename))
3502 if cwd == root:
3503 break
3504 cwd = os.path.dirname(cwd)
3505
3506
3507def LoadCodereviewSettingsFromFile(fileobj):
3508 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003509 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003510
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003511 def SetProperty(name, setting, unset_error_ok=False):
3512 fullname = 'rietveld.' + name
3513 if setting in keyvals:
3514 RunGit(['config', fullname, keyvals[setting]])
3515 else:
3516 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3517
tandrii48df5812016-10-17 03:55:37 -07003518 if not keyvals.get('GERRIT_HOST', False):
3519 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003520 # Only server setting is required. Other settings can be absent.
3521 # In that case, we ignore errors raised during option deletion attempt.
3522 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003523 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003524 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3525 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003526 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003527 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3528 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003529 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003530 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3531 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003532
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003533 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003534 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003535
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003536 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003537 RunGit(['config', 'gerrit.squash-uploads',
3538 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003539
tandrii@chromium.org28253532016-04-14 13:46:56 +00003540 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003541 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003542 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3543
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003544 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003545 # should be of the form
3546 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3547 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003548 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3549 keyvals['ORIGIN_URL_CONFIG']])
3550
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003551
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003552def urlretrieve(source, destination):
3553 """urllib is broken for SSL connections via a proxy therefore we
3554 can't use urllib.urlretrieve()."""
3555 with open(destination, 'w') as f:
3556 f.write(urllib2.urlopen(source).read())
3557
3558
ukai@chromium.org712d6102013-11-27 00:52:58 +00003559def hasSheBang(fname):
3560 """Checks fname is a #! script."""
3561 with open(fname) as f:
3562 return f.read(2).startswith('#!')
3563
3564
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003565# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3566def DownloadHooks(*args, **kwargs):
3567 pass
3568
3569
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003570def DownloadGerritHook(force):
3571 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003572
3573 Args:
3574 force: True to update hooks. False to install hooks if not present.
3575 """
3576 if not settings.GetIsGerrit():
3577 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003578 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003579 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3580 if not os.access(dst, os.X_OK):
3581 if os.path.exists(dst):
3582 if not force:
3583 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003584 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003585 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003586 if not hasSheBang(dst):
3587 DieWithError('Not a script: %s\n'
3588 'You need to download from\n%s\n'
3589 'into .git/hooks/commit-msg and '
3590 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003591 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3592 except Exception:
3593 if os.path.exists(dst):
3594 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003595 DieWithError('\nFailed to download hooks.\n'
3596 'You need to download from\n%s\n'
3597 'into .git/hooks/commit-msg and '
3598 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003599
3600
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003601def GetRietveldCodereviewSettingsInteractively():
3602 """Prompt the user for settings."""
3603 server = settings.GetDefaultServerUrl(error_ok=True)
3604 prompt = 'Rietveld server (host[:port])'
3605 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3606 newserver = ask_for_data(prompt + ':')
3607 if not server and not newserver:
3608 newserver = DEFAULT_SERVER
3609 if newserver:
3610 newserver = gclient_utils.UpgradeToHttps(newserver)
3611 if newserver != server:
3612 RunGit(['config', 'rietveld.server', newserver])
3613
3614 def SetProperty(initial, caption, name, is_url):
3615 prompt = caption
3616 if initial:
3617 prompt += ' ("x" to clear) [%s]' % initial
3618 new_val = ask_for_data(prompt + ':')
3619 if new_val == 'x':
3620 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3621 elif new_val:
3622 if is_url:
3623 new_val = gclient_utils.UpgradeToHttps(new_val)
3624 if new_val != initial:
3625 RunGit(['config', 'rietveld.' + name, new_val])
3626
3627 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3628 SetProperty(settings.GetDefaultPrivateFlag(),
3629 'Private flag (rietveld only)', 'private', False)
3630 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3631 'tree-status-url', False)
3632 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3633 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3634 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3635 'run-post-upload-hook', False)
3636
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003637
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003638class _GitCookiesChecker(object):
3639 """Provides facilties for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003640
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003641 _GOOGLESOURCE = 'googlesource.com'
3642
3643 def __init__(self):
3644 # Cached list of [host, identity, source], where source is either
3645 # .gitcookies or .netrc.
3646 self._all_hosts = None
3647
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003648 def ensure_configured_gitcookies(self):
3649 """Runs checks and suggests fixes to make git use .gitcookies from default
3650 path."""
3651 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3652 configured_path = RunGitSilent(
3653 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003654 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003655 if configured_path:
3656 self._ensure_default_gitcookies_path(configured_path, default)
3657 else:
3658 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003659
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003660 @staticmethod
3661 def _ensure_default_gitcookies_path(configured_path, default_path):
3662 assert configured_path
3663 if configured_path == default_path:
3664 print('git is already configured to use your .gitcookies from %s' %
3665 configured_path)
3666 return
3667
3668 print('WARNING: you have configured custom path to .gitcookies: %s\n'
3669 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3670 (configured_path, default_path))
3671
3672 if not os.path.exists(configured_path):
3673 print('However, your configured .gitcookies file is missing.')
3674 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3675 action='reconfigure')
3676 RunGit(['config', '--global', 'http.cookiefile', default_path])
3677 return
3678
3679 if os.path.exists(default_path):
3680 print('WARNING: default .gitcookies file already exists %s' %
3681 default_path)
3682 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3683 default_path)
3684
3685 confirm_or_exit('Move existing .gitcookies to default location?',
3686 action='move')
3687 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003688 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003689 print('Moved and reconfigured git to use .gitcookies from %s' %
3690 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003691
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003692 @staticmethod
3693 def _configure_gitcookies_path(default_path):
3694 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3695 if os.path.exists(netrc_path):
3696 print('You seem to be using outdated .netrc for git credentials: %s' %
3697 netrc_path)
3698 print('This tool will guide you through setting up recommended '
3699 '.gitcookies store for git credentials.\n'
3700 '\n'
3701 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3702 ' git config --global --unset http.cookiefile\n'
3703 ' mv %s %s.backup\n\n' % (default_path, default_path))
3704 confirm_or_exit(action='setup .gitcookies')
3705 RunGit(['config', '--global', 'http.cookiefile', default_path])
3706 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003707
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003708 def get_hosts_with_creds(self, include_netrc=False):
3709 if self._all_hosts is None:
3710 a = gerrit_util.CookiesAuthenticator()
3711 self._all_hosts = [
3712 (h, u, s)
3713 for h, u, s in itertools.chain(
3714 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3715 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3716 )
3717 if h.endswith(self._GOOGLESOURCE)
3718 ]
3719
3720 if include_netrc:
3721 return self._all_hosts
3722 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3723
3724 def print_current_creds(self, include_netrc=False):
3725 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3726 if not hosts:
3727 print('No Git/Gerrit credentials found')
3728 return
3729 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3730 header = [('Host', 'User', 'Which file'),
3731 ['=' * l for l in lengths]]
3732 for row in (header + hosts):
3733 print('\t'.join((('%%+%ds' % l) % s)
3734 for l, s in zip(lengths, row)))
3735
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003736 @staticmethod
3737 def _parse_identity(identity):
3738 """Parses identity "git-<ldap>.example.com" into <ldap> and domain."""
3739 username, domain = identity.split('.', 1)
3740 if username.startswith('git-'):
3741 username = username[len('git-'):]
3742 return username, domain
3743
3744 def _get_usernames_of_domain(self, domain):
3745 """Returns list of usernames referenced by .gitcookies in a given domain."""
3746 identities_by_domain = {}
3747 for _, identity, _ in self.get_hosts_with_creds():
3748 username, domain = self._parse_identity(identity)
3749 identities_by_domain.setdefault(domain, []).append(username)
3750 return identities_by_domain.get(domain)
3751
3752 def _canonical_git_googlesource_host(self, host):
3753 """Normalizes Gerrit hosts (with '-review') to Git host."""
3754 assert host.endswith(self._GOOGLESOURCE)
3755 # Prefix doesn't include '.' at the end.
3756 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3757 if prefix.endswith('-review'):
3758 prefix = prefix[:-len('-review')]
3759 return prefix + '.' + self._GOOGLESOURCE
3760
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003761 def _canonical_gerrit_googlesource_host(self, host):
3762 git_host = self._canonical_git_googlesource_host(host)
3763 prefix = git_host.split('.', 1)[0]
3764 return prefix + '-review.' + self._GOOGLESOURCE
3765
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003766 def has_generic_host(self):
3767 """Returns whether generic .googlesource.com has been configured.
3768
3769 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3770 """
3771 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3772 if host == '.' + self._GOOGLESOURCE:
3773 return True
3774 return False
3775
3776 def _get_git_gerrit_identity_pairs(self):
3777 """Returns map from canonic host to pair of identities (Git, Gerrit).
3778
3779 One of identities might be None, meaning not configured.
3780 """
3781 host_to_identity_pairs = {}
3782 for host, identity, _ in self.get_hosts_with_creds():
3783 canonical = self._canonical_git_googlesource_host(host)
3784 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3785 idx = 0 if canonical == host else 1
3786 pair[idx] = identity
3787 return host_to_identity_pairs
3788
3789 def get_partially_configured_hosts(self):
3790 return set(
3791 host for host, identities_pair in
3792 self._get_git_gerrit_identity_pairs().iteritems()
3793 if None in identities_pair and host != '.' + self._GOOGLESOURCE)
3794
3795 def get_conflicting_hosts(self):
3796 return set(
3797 host for host, (i1, i2) in
3798 self._get_git_gerrit_identity_pairs().iteritems()
3799 if None not in (i1, i2) and i1 != i2)
3800
3801 def get_duplicated_hosts(self):
3802 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3803 return set(host for host, count in counters.iteritems() if count > 1)
3804
3805 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3806 'chromium.googlesource.com': 'chromium.org',
3807 'chrome-internal.googlesource.com': 'google.com',
3808 }
3809
3810 def get_hosts_with_wrong_identities(self):
3811 """Finds hosts which **likely** reference wrong identities.
3812
3813 Note: skips hosts which have conflicting identities for Git and Gerrit.
3814 """
3815 hosts = set()
3816 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3817 pair = self._get_git_gerrit_identity_pairs().get(host)
3818 if pair and pair[0] == pair[1]:
3819 _, domain = self._parse_identity(pair[0])
3820 if domain != expected:
3821 hosts.add(host)
3822 return hosts
3823
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003824 @staticmethod
3825 def print_hosts(hosts, extra_column_func=None):
3826 hosts = sorted(hosts)
3827 assert hosts
3828 if extra_column_func is None:
3829 extras = [''] * len(hosts)
3830 else:
3831 extras = [extra_column_func(host) for host in hosts]
3832 tmpl = ' %%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3833 for he in zip(hosts, extras):
3834 print(tmpl % he)
3835 print()
3836
3837 def find_and_report_problems(self):
3838 """Returns True if there was at least one problem, else False."""
3839 problems = [False]
3840 def add_problem():
3841 if not problems[0]:
Andrii Shyshkalov4812e612017-03-27 17:22:57 +02003842 print('\n\n.gitcookies problem report:\n')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003843 problems[0] = True
3844
3845 if self.has_generic_host():
3846 add_problem()
3847 print(' .googlesource.com record detected\n'
3848 ' Chrome Infrastructure team recommends to list full host names '
3849 'explicitly.\n')
3850
3851 dups = self.get_duplicated_hosts()
3852 if dups:
3853 add_problem()
3854 print(' The following hosts were defined twice:\n')
3855 self.print_hosts(dups)
3856
3857 partial = self.get_partially_configured_hosts()
3858 if partial:
3859 add_problem()
3860 print(' Credentials should come in pairs for Git and Gerrit hosts. '
3861 'These hosts are missing:')
3862 self.print_hosts(partial)
3863
3864 conflicting = self.get_conflicting_hosts()
3865 if conflicting:
3866 add_problem()
3867 print(' The following Git hosts have differing credentials from their '
3868 'Gerrit counterparts:\n')
3869 self.print_hosts(conflicting, lambda host: '%s vs %s' %
3870 tuple(self._get_git_gerrit_identity_pairs()[host]))
3871
3872 wrong = self.get_hosts_with_wrong_identities()
3873 if wrong:
3874 add_problem()
3875 print(' These hosts likely use wrong identity:\n')
3876 self.print_hosts(wrong, lambda host: '%s but %s recommended' %
3877 (self._get_git_gerrit_identity_pairs()[host][0],
3878 self._EXPECTED_HOST_IDENTITY_DOMAINS[host]))
3879 return problems[0]
3880
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003881
3882def CMDcreds_check(parser, args):
3883 """Checks credentials and suggests changes."""
3884 _, _ = parser.parse_args(args)
3885
3886 if gerrit_util.GceAuthenticator.is_gce():
3887 DieWithError('this command is not designed for GCE, are you on a bot?')
3888
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003889 checker = _GitCookiesChecker()
3890 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003891
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003892 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003893 checker.print_current_creds(include_netrc=True)
3894
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003895 if not checker.find_and_report_problems():
3896 print('\nNo problems detected in your .gitcookies')
3897 return 0
3898 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003899
3900
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003901@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003902def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003903 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003904
tandrii5d0a0422016-09-14 06:24:35 -07003905 print('WARNING: git cl config works for Rietveld only')
3906 # TODO(tandrii): remove this once we switch to Gerrit.
3907 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003908 parser.add_option('--activate-update', action='store_true',
3909 help='activate auto-updating [rietveld] section in '
3910 '.git/config')
3911 parser.add_option('--deactivate-update', action='store_true',
3912 help='deactivate auto-updating [rietveld] section in '
3913 '.git/config')
3914 options, args = parser.parse_args(args)
3915
3916 if options.deactivate_update:
3917 RunGit(['config', 'rietveld.autoupdate', 'false'])
3918 return
3919
3920 if options.activate_update:
3921 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3922 return
3923
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003924 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003925 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003926 return 0
3927
3928 url = args[0]
3929 if not url.endswith('codereview.settings'):
3930 url = os.path.join(url, 'codereview.settings')
3931
3932 # Load code review settings and download hooks (if available).
3933 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3934 return 0
3935
3936
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003937def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003938 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003939 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3940 branch = ShortBranchName(branchref)
3941 _, args = parser.parse_args(args)
3942 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003943 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003944 return RunGit(['config', 'branch.%s.base-url' % branch],
3945 error_ok=False).strip()
3946 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003947 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003948 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3949 error_ok=False).strip()
3950
3951
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003952def color_for_status(status):
3953 """Maps a Changelist status to color, for CMDstatus and other tools."""
3954 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003955 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003956 'waiting': Fore.BLUE,
3957 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003958 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003959 'lgtm': Fore.GREEN,
3960 'commit': Fore.MAGENTA,
3961 'closed': Fore.CYAN,
3962 'error': Fore.WHITE,
3963 }.get(status, Fore.WHITE)
3964
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003965
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003966def get_cl_statuses(changes, fine_grained, max_processes=None):
3967 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003968
3969 If fine_grained is true, this will fetch CL statuses from the server.
3970 Otherwise, simply indicate if there's a matching url for the given branches.
3971
3972 If max_processes is specified, it is used as the maximum number of processes
3973 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3974 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003975
3976 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003977 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003978 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003979 upload.verbosity = 0
3980
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003981 if not changes:
3982 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003983
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003984 if not fine_grained:
3985 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003986 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003987 for cl in changes:
3988 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003989 return
3990
3991 # First, sort out authentication issues.
3992 logging.debug('ensuring credentials exist')
3993 for cl in changes:
3994 cl.EnsureAuthenticated(force=False, refresh=True)
3995
3996 def fetch(cl):
3997 try:
3998 return (cl, cl.GetStatus())
3999 except:
4000 # See http://crbug.com/629863.
4001 logging.exception('failed to fetch status for %s:', cl)
4002 raise
4003
4004 threads_count = len(changes)
4005 if max_processes:
4006 threads_count = max(1, min(threads_count, max_processes))
4007 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4008
4009 pool = ThreadPool(threads_count)
4010 fetched_cls = set()
4011 try:
4012 it = pool.imap_unordered(fetch, changes).__iter__()
4013 while True:
4014 try:
4015 cl, status = it.next(timeout=5)
4016 except multiprocessing.TimeoutError:
4017 break
4018 fetched_cls.add(cl)
4019 yield cl, status
4020 finally:
4021 pool.close()
4022
4023 # Add any branches that failed to fetch.
4024 for cl in set(changes) - fetched_cls:
4025 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004026
rmistry@google.com2dd99862015-06-22 12:22:18 +00004027
4028def upload_branch_deps(cl, args):
4029 """Uploads CLs of local branches that are dependents of the current branch.
4030
4031 If the local branch dependency tree looks like:
4032 test1 -> test2.1 -> test3.1
4033 -> test3.2
4034 -> test2.2 -> test3.3
4035
4036 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4037 run on the dependent branches in this order:
4038 test2.1, test3.1, test3.2, test2.2, test3.3
4039
4040 Note: This function does not rebase your local dependent branches. Use it when
4041 you make a change to the parent branch that will not conflict with its
4042 dependent branches, and you would like their dependencies updated in
4043 Rietveld.
4044 """
4045 if git_common.is_dirty_git_tree('upload-branch-deps'):
4046 return 1
4047
4048 root_branch = cl.GetBranch()
4049 if root_branch is None:
4050 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4051 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004052 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004053 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4054 'patchset dependencies without an uploaded CL.')
4055
4056 branches = RunGit(['for-each-ref',
4057 '--format=%(refname:short) %(upstream:short)',
4058 'refs/heads'])
4059 if not branches:
4060 print('No local branches found.')
4061 return 0
4062
4063 # Create a dictionary of all local branches to the branches that are dependent
4064 # on it.
4065 tracked_to_dependents = collections.defaultdict(list)
4066 for b in branches.splitlines():
4067 tokens = b.split()
4068 if len(tokens) == 2:
4069 branch_name, tracked = tokens
4070 tracked_to_dependents[tracked].append(branch_name)
4071
vapiera7fbd5a2016-06-16 09:17:49 -07004072 print()
4073 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004074 dependents = []
4075 def traverse_dependents_preorder(branch, padding=''):
4076 dependents_to_process = tracked_to_dependents.get(branch, [])
4077 padding += ' '
4078 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004079 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004080 dependents.append(dependent)
4081 traverse_dependents_preorder(dependent, padding)
4082 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004083 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004084
4085 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004086 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004087 return 0
4088
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004089 confirm_or_exit('This command will checkout all dependent branches and run '
4090 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004091
andybons@chromium.org962f9462016-02-03 20:00:42 +00004092 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004093 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004094 args.extend(['-t', 'Updated patchset dependency'])
4095
rmistry@google.com2dd99862015-06-22 12:22:18 +00004096 # Record all dependents that failed to upload.
4097 failures = {}
4098 # Go through all dependents, checkout the branch and upload.
4099 try:
4100 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004101 print()
4102 print('--------------------------------------')
4103 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004104 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004105 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004106 try:
4107 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004108 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004109 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004110 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004111 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004112 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004113 finally:
4114 # Swap back to the original root branch.
4115 RunGit(['checkout', '-q', root_branch])
4116
vapiera7fbd5a2016-06-16 09:17:49 -07004117 print()
4118 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004119 for dependent_branch in dependents:
4120 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004121 print(' %s : %s' % (dependent_branch, upload_status))
4122 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004123
4124 return 0
4125
4126
kmarshall3bff56b2016-06-06 18:31:47 -07004127def CMDarchive(parser, args):
4128 """Archives and deletes branches associated with closed changelists."""
4129 parser.add_option(
4130 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004131 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004132 parser.add_option(
4133 '-f', '--force', action='store_true',
4134 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004135 parser.add_option(
4136 '-d', '--dry-run', action='store_true',
4137 help='Skip the branch tagging and removal steps.')
4138 parser.add_option(
4139 '-t', '--notags', action='store_true',
4140 help='Do not tag archived branches. '
4141 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004142
4143 auth.add_auth_options(parser)
4144 options, args = parser.parse_args(args)
4145 if args:
4146 parser.error('Unsupported args: %s' % ' '.join(args))
4147 auth_config = auth.extract_auth_config_from_options(options)
4148
4149 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4150 if not branches:
4151 return 0
4152
vapiera7fbd5a2016-06-16 09:17:49 -07004153 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004154 changes = [Changelist(branchref=b, auth_config=auth_config)
4155 for b in branches.splitlines()]
4156 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4157 statuses = get_cl_statuses(changes,
4158 fine_grained=True,
4159 max_processes=options.maxjobs)
4160 proposal = [(cl.GetBranch(),
4161 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4162 for cl, status in statuses
4163 if status == 'closed']
4164 proposal.sort()
4165
4166 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004167 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004168 return 0
4169
4170 current_branch = GetCurrentBranch()
4171
vapiera7fbd5a2016-06-16 09:17:49 -07004172 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004173 if options.notags:
4174 for next_item in proposal:
4175 print(' ' + next_item[0])
4176 else:
4177 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4178 for next_item in proposal:
4179 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004180
kmarshall9249e012016-08-23 12:02:16 -07004181 # Quit now on precondition failure or if instructed by the user, either
4182 # via an interactive prompt or by command line flags.
4183 if options.dry_run:
4184 print('\nNo changes were made (dry run).\n')
4185 return 0
4186 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004187 print('You are currently on a branch \'%s\' which is associated with a '
4188 'closed codereview issue, so archive cannot proceed. Please '
4189 'checkout another branch and run this command again.' %
4190 current_branch)
4191 return 1
kmarshall9249e012016-08-23 12:02:16 -07004192 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004193 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4194 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004195 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004196 return 1
4197
4198 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004199 if not options.notags:
4200 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004201 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004202
vapiera7fbd5a2016-06-16 09:17:49 -07004203 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004204
4205 return 0
4206
4207
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004208def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004209 """Show status of changelists.
4210
4211 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004212 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004213 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004214 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004215 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004216 - Magenta in the commit queue
4217 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004218 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004219
4220 Also see 'git cl comments'.
4221 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004222 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004223 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004224 parser.add_option('-f', '--fast', action='store_true',
4225 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004226 parser.add_option(
4227 '-j', '--maxjobs', action='store', type=int,
4228 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004229
4230 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004231 _add_codereview_issue_select_options(
4232 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004233 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004234 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004235 if args:
4236 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004237 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004238
iannuccie53c9352016-08-17 14:40:40 -07004239 if options.issue is not None and not options.field:
4240 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004241
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004242 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004243 cl = Changelist(auth_config=auth_config, issue=options.issue,
4244 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004245 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004246 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004247 elif options.field == 'id':
4248 issueid = cl.GetIssue()
4249 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004250 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004251 elif options.field == 'patch':
4252 patchset = cl.GetPatchset()
4253 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004254 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004255 elif options.field == 'status':
4256 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004257 elif options.field == 'url':
4258 url = cl.GetIssueURL()
4259 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004260 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004261 return 0
4262
4263 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4264 if not branches:
4265 print('No local branch found.')
4266 return 0
4267
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004268 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004269 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004270 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004271 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004272 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004273 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004274 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004275
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004276 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004277 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4278 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4279 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004280 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004281 c, status = output.next()
4282 branch_statuses[c.GetBranch()] = status
4283 status = branch_statuses.pop(branch)
4284 url = cl.GetIssueURL()
4285 if url and (not status or status == 'error'):
4286 # The issue probably doesn't exist anymore.
4287 url += ' (broken)'
4288
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004289 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004290 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004291 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004292 color = ''
4293 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004294 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004295 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004296 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004297 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004298
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004299
4300 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004301 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004302 print('Current branch: %s' % branch)
4303 for cl in changes:
4304 if cl.GetBranch() == branch:
4305 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004306 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004307 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004308 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004309 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004310 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004311 print('Issue description:')
4312 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004313 return 0
4314
4315
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004316def colorize_CMDstatus_doc():
4317 """To be called once in main() to add colors to git cl status help."""
4318 colors = [i for i in dir(Fore) if i[0].isupper()]
4319
4320 def colorize_line(line):
4321 for color in colors:
4322 if color in line.upper():
4323 # Extract whitespaces first and the leading '-'.
4324 indent = len(line) - len(line.lstrip(' ')) + 1
4325 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4326 return line
4327
4328 lines = CMDstatus.__doc__.splitlines()
4329 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4330
4331
phajdan.jre328cf92016-08-22 04:12:17 -07004332def write_json(path, contents):
4333 with open(path, 'w') as f:
4334 json.dump(contents, f)
4335
4336
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004337@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004338def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004339 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004340
4341 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004342 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004343 parser.add_option('-r', '--reverse', action='store_true',
4344 help='Lookup the branch(es) for the specified issues. If '
4345 'no issues are specified, all branches with mapped '
4346 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07004347 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004348 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004349 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004350 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004351
dnj@chromium.org406c4402015-03-03 17:22:28 +00004352 if options.reverse:
4353 branches = RunGit(['for-each-ref', 'refs/heads',
4354 '--format=%(refname:short)']).splitlines()
4355
4356 # Reverse issue lookup.
4357 issue_branch_map = {}
4358 for branch in branches:
4359 cl = Changelist(branchref=branch)
4360 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4361 if not args:
4362 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004363 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004364 for issue in args:
4365 if not issue:
4366 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004367 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004368 print('Branch for issue number %s: %s' % (
4369 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004370 if options.json:
4371 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004372 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004373 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004374 if len(args) > 0:
4375 try:
4376 issue = int(args[0])
4377 except ValueError:
4378 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00004379 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00004380 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07004381 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07004382 if options.json:
4383 write_json(options.json, {
4384 'issue': cl.GetIssue(),
4385 'issue_url': cl.GetIssueURL(),
4386 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004387 return 0
4388
4389
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004390def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004391 """Shows or posts review comments for any changelist."""
4392 parser.add_option('-a', '--add-comment', dest='comment',
4393 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004394 parser.add_option('-i', '--issue', dest='issue',
4395 help='review issue id (defaults to current issue). '
4396 'If given, requires --rietveld or --gerrit')
smut@google.comc85ac942015-09-15 16:34:43 +00004397 parser.add_option('-j', '--json-file',
4398 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004399 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004400 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004401 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004402 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004403 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004404
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004405 issue = None
4406 if options.issue:
4407 try:
4408 issue = int(options.issue)
4409 except ValueError:
4410 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004411 if not options.forced_codereview:
4412 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004413
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004414 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004415 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004416 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004417
4418 if options.comment:
4419 cl.AddComment(options.comment)
4420 return 0
4421
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004422 summary = sorted(cl.GetCommentsSummary(), key=lambda c: c.date)
4423 for comment in summary:
4424 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004425 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004426 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004427 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004428 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004429 color = Fore.MAGENTA
4430 else:
4431 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004432 print('\n%s%s %s%s\n%s' % (
4433 color,
4434 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4435 comment.sender,
4436 Fore.RESET,
4437 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4438
smut@google.comc85ac942015-09-15 16:34:43 +00004439 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004440 def pre_serialize(c):
4441 dct = c.__dict__.copy()
4442 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4443 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004444 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004445 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004446 return 0
4447
4448
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004449@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004450def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004451 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004452 parser.add_option('-d', '--display', action='store_true',
4453 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004454 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004455 help='New description to set for this issue (- for stdin, '
4456 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004457 parser.add_option('-f', '--force', action='store_true',
4458 help='Delete any unpublished Gerrit edits for this issue '
4459 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004460
4461 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004462 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004463 options, args = parser.parse_args(args)
4464 _process_codereview_select_options(parser, options)
4465
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004466 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004467 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004468 target_issue_arg = ParseIssueNumberArgument(args[0],
4469 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004470 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004471 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004472
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004473 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004474
martiniss6eda05f2016-06-30 10:18:35 -07004475 kwargs = {
4476 'auth_config': auth_config,
4477 'codereview': options.forced_codereview,
4478 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004479 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004480 if target_issue_arg:
4481 kwargs['issue'] = target_issue_arg.issue
4482 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004483 if target_issue_arg.codereview and not options.forced_codereview:
4484 detected_codereview_from_url = True
4485 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004486
4487 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004488 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004489 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004490 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004491
4492 if detected_codereview_from_url:
4493 logging.info('canonical issue/change URL: %s (type: %s)\n',
4494 cl.GetIssueURL(), target_issue_arg.codereview)
4495
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004496 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004497
smut@google.com34fb6b12015-07-13 20:03:26 +00004498 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004499 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004500 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004501
4502 if options.new_description:
4503 text = options.new_description
4504 if text == '-':
4505 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004506 elif text == '+':
4507 base_branch = cl.GetCommonAncestorWithUpstream()
4508 change = cl.GetChange(base_branch, None, local_description=True)
4509 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004510
4511 description.set_description(text)
4512 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004513 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004514
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004515 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004516 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004517 return 0
4518
4519
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004520def CreateDescriptionFromLog(args):
4521 """Pulls out the commit log to use as a base for the CL description."""
4522 log_args = []
4523 if len(args) == 1 and not args[0].endswith('.'):
4524 log_args = [args[0] + '..']
4525 elif len(args) == 1 and args[0].endswith('...'):
4526 log_args = [args[0][:-1]]
4527 elif len(args) == 2:
4528 log_args = [args[0] + '..' + args[1]]
4529 else:
4530 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004531 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004532
4533
thestig@chromium.org44202a22014-03-11 19:22:18 +00004534def CMDlint(parser, args):
4535 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004536 parser.add_option('--filter', action='append', metavar='-x,+y',
4537 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004538 auth.add_auth_options(parser)
4539 options, args = parser.parse_args(args)
4540 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004541
4542 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004543 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004544 try:
4545 import cpplint
4546 import cpplint_chromium
4547 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004548 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004549 return 1
4550
4551 # Change the current working directory before calling lint so that it
4552 # shows the correct base.
4553 previous_cwd = os.getcwd()
4554 os.chdir(settings.GetRoot())
4555 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004556 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004557 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4558 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004559 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004560 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004561 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004562
4563 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004564 command = args + files
4565 if options.filter:
4566 command = ['--filter=' + ','.join(options.filter)] + command
4567 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004568
4569 white_regex = re.compile(settings.GetLintRegex())
4570 black_regex = re.compile(settings.GetLintIgnoreRegex())
4571 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4572 for filename in filenames:
4573 if white_regex.match(filename):
4574 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004575 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004576 else:
4577 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4578 extra_check_functions)
4579 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004580 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004581 finally:
4582 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004583 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004584 if cpplint._cpplint_state.error_count != 0:
4585 return 1
4586 return 0
4587
4588
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004589def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004590 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004591 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004592 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004593 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004594 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004595 auth.add_auth_options(parser)
4596 options, args = parser.parse_args(args)
4597 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004598
sbc@chromium.org71437c02015-04-09 19:29:40 +00004599 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004600 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004601 return 1
4602
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004603 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004604 if args:
4605 base_branch = args[0]
4606 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004607 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004608 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004609
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004610 cl.RunHook(
4611 committing=not options.upload,
4612 may_prompt=False,
4613 verbose=options.verbose,
4614 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004615 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004616
4617
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004618def GenerateGerritChangeId(message):
4619 """Returns Ixxxxxx...xxx change id.
4620
4621 Works the same way as
4622 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4623 but can be called on demand on all platforms.
4624
4625 The basic idea is to generate git hash of a state of the tree, original commit
4626 message, author/committer info and timestamps.
4627 """
4628 lines = []
4629 tree_hash = RunGitSilent(['write-tree'])
4630 lines.append('tree %s' % tree_hash.strip())
4631 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4632 if code == 0:
4633 lines.append('parent %s' % parent.strip())
4634 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4635 lines.append('author %s' % author.strip())
4636 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4637 lines.append('committer %s' % committer.strip())
4638 lines.append('')
4639 # Note: Gerrit's commit-hook actually cleans message of some lines and
4640 # whitespace. This code is not doing this, but it clearly won't decrease
4641 # entropy.
4642 lines.append(message)
4643 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4644 stdin='\n'.join(lines))
4645 return 'I%s' % change_hash.strip()
4646
4647
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004648def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004649 """Computes the remote branch ref to use for the CL.
4650
4651 Args:
4652 remote (str): The git remote for the CL.
4653 remote_branch (str): The git remote branch for the CL.
4654 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004655 """
4656 if not (remote and remote_branch):
4657 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004658
wittman@chromium.org455dc922015-01-26 20:15:50 +00004659 if target_branch:
4660 # Cannonicalize branch references to the equivalent local full symbolic
4661 # refs, which are then translated into the remote full symbolic refs
4662 # below.
4663 if '/' not in target_branch:
4664 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4665 else:
4666 prefix_replacements = (
4667 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4668 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4669 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4670 )
4671 match = None
4672 for regex, replacement in prefix_replacements:
4673 match = re.search(regex, target_branch)
4674 if match:
4675 remote_branch = target_branch.replace(match.group(0), replacement)
4676 break
4677 if not match:
4678 # This is a branch path but not one we recognize; use as-is.
4679 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004680 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4681 # Handle the refs that need to land in different refs.
4682 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004683
wittman@chromium.org455dc922015-01-26 20:15:50 +00004684 # Create the true path to the remote branch.
4685 # Does the following translation:
4686 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4687 # * refs/remotes/origin/master -> refs/heads/master
4688 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4689 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4690 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4691 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4692 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4693 'refs/heads/')
4694 elif remote_branch.startswith('refs/remotes/branch-heads'):
4695 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004696
wittman@chromium.org455dc922015-01-26 20:15:50 +00004697 return remote_branch
4698
4699
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004700def cleanup_list(l):
4701 """Fixes a list so that comma separated items are put as individual items.
4702
4703 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4704 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4705 """
4706 items = sum((i.split(',') for i in l), [])
4707 stripped_items = (i.strip() for i in items)
4708 return sorted(filter(None, stripped_items))
4709
4710
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004711@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004712def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004713 """Uploads the current changelist to codereview.
4714
4715 Can skip dependency patchset uploads for a branch by running:
4716 git config branch.branch_name.skip-deps-uploads True
4717 To unset run:
4718 git config --unset branch.branch_name.skip-deps-uploads
4719 Can also set the above globally by using the --global flag.
4720 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004721 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4722 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004723 parser.add_option('--bypass-watchlists', action='store_true',
4724 dest='bypass_watchlists',
4725 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004726 parser.add_option('-f', action='store_true', dest='force',
4727 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004728 parser.add_option('--message', '-m', dest='message',
4729 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004730 parser.add_option('-b', '--bug',
4731 help='pre-populate the bug number(s) for this issue. '
4732 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004733 parser.add_option('--message-file', dest='message_file',
4734 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004735 parser.add_option('--title', '-t', dest='title',
4736 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004737 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004738 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004739 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004740 parser.add_option('--tbrs',
4741 action='append', default=[],
4742 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004743 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004744 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004745 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004746 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004747 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004748 parser.add_option('--emulate_svn_auto_props',
4749 '--emulate-svn-auto-props',
4750 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004751 dest="emulate_svn_auto_props",
4752 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004753 parser.add_option('-c', '--use-commit-queue', action='store_true',
4754 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004755 parser.add_option('--private', action='store_true',
4756 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004757 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004758 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004759 metavar='TARGET',
4760 help='Apply CL to remote ref TARGET. ' +
4761 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004762 parser.add_option('--squash', action='store_true',
4763 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004764 parser.add_option('--no-squash', action='store_true',
4765 help='Don\'t squash multiple commits into one ' +
4766 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004767 parser.add_option('--topic', default=None,
4768 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004769 parser.add_option('--email', default=None,
4770 help='email address to use to connect to Rietveld')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004771 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4772 const='TBR', help='add a set of OWNERS to TBR')
4773 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4774 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004775 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4776 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004777 help='Send the patchset to do a CQ dry run right after '
4778 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004779 parser.add_option('--dependencies', action='store_true',
4780 help='Uploads CLs of all the local branches that depend on '
4781 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004782
rmistry@google.com2dd99862015-06-22 12:22:18 +00004783 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004784 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004785 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004786 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004787 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004788 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004789 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004790
sbc@chromium.org71437c02015-04-09 19:29:40 +00004791 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004792 return 1
4793
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004794 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004795 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004796 options.cc = cleanup_list(options.cc)
4797
tandriib80458a2016-06-23 12:20:07 -07004798 if options.message_file:
4799 if options.message:
4800 parser.error('only one of --message and --message-file allowed.')
4801 options.message = gclient_utils.FileRead(options.message_file)
4802 options.message_file = None
4803
tandrii4d0545a2016-07-06 03:56:49 -07004804 if options.cq_dry_run and options.use_commit_queue:
4805 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4806
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004807 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4808 settings.GetIsGerrit()
4809
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004810 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004811 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004812
4813
Francois Dorayd42c6812017-05-30 15:10:20 -04004814@subcommand.usage('--description=<description file>')
4815def CMDsplit(parser, args):
4816 """Splits a branch into smaller branches and uploads CLs.
4817
4818 Creates a branch and uploads a CL for each group of files modified in the
4819 current branch that share a common OWNERS file. In the CL description and
4820 commment, the string '$directory', is replaced with the directory containing
4821 the shared OWNERS file.
4822 """
4823 parser.add_option("-d", "--description", dest="description_file",
4824 help="A text file containing a CL description. ")
4825 parser.add_option("-c", "--comment", dest="comment_file",
4826 help="A text file containing a CL comment.")
4827 options, _ = parser.parse_args(args)
4828
4829 if not options.description_file:
4830 parser.error('No --description flag specified.')
4831
4832 def WrappedCMDupload(args):
4833 return CMDupload(OptionParser(), args)
4834
4835 return split_cl.SplitCl(options.description_file, options.comment_file,
4836 Changelist, WrappedCMDupload)
4837
4838
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004839@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004840def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004841 """DEPRECATED: Used to commit the current changelist via git-svn."""
4842 message = ('git-cl no longer supports committing to SVN repositories via '
4843 'git-svn. You probably want to use `git cl land` instead.')
4844 print(message)
4845 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004846
4847
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004848# Two special branches used by git cl land.
4849MERGE_BRANCH = 'git-cl-commit'
4850CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4851
4852
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004853@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004854def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004855 """Commits the current changelist via git.
4856
4857 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4858 upstream and closes the issue automatically and atomically.
4859
4860 Otherwise (in case of Rietveld):
4861 Squashes branch into a single commit.
4862 Updates commit message with metadata (e.g. pointer to review).
4863 Pushes the code upstream.
4864 Updates review and closes.
4865 """
4866 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4867 help='bypass upload presubmit hook')
4868 parser.add_option('-m', dest='message',
4869 help="override review description")
4870 parser.add_option('-f', action='store_true', dest='force',
4871 help="force yes to questions (don't prompt)")
4872 parser.add_option('-c', dest='contributor',
4873 help="external contributor for patch (appended to " +
4874 "description and used as author for git). Should be " +
4875 "formatted as 'First Last <email@example.com>'")
4876 add_git_similarity(parser)
4877 auth.add_auth_options(parser)
4878 (options, args) = parser.parse_args(args)
4879 auth_config = auth.extract_auth_config_from_options(options)
4880
4881 cl = Changelist(auth_config=auth_config)
4882
4883 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4884 if cl.IsGerrit():
4885 if options.message:
4886 # This could be implemented, but it requires sending a new patch to
4887 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4888 # Besides, Gerrit has the ability to change the commit message on submit
4889 # automatically, thus there is no need to support this option (so far?).
4890 parser.error('-m MESSAGE option is not supported for Gerrit.')
4891 if options.contributor:
4892 parser.error(
4893 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4894 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4895 'the contributor\'s "name <email>". If you can\'t upload such a '
4896 'commit for review, contact your repository admin and request'
4897 '"Forge-Author" permission.')
4898 if not cl.GetIssue():
4899 DieWithError('You must upload the change first to Gerrit.\n'
4900 ' If you would rather have `git cl land` upload '
4901 'automatically for you, see http://crbug.com/642759')
4902 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4903 options.verbose)
4904
4905 current = cl.GetBranch()
4906 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4907 if remote == '.':
4908 print()
4909 print('Attempting to push branch %r into another local branch!' % current)
4910 print()
4911 print('Either reparent this branch on top of origin/master:')
4912 print(' git reparent-branch --root')
4913 print()
4914 print('OR run `git rebase-update` if you think the parent branch is ')
4915 print('already committed.')
4916 print()
4917 print(' Current parent: %r' % upstream_branch)
4918 return 1
4919
4920 if not args:
4921 # Default to merging against our best guess of the upstream branch.
4922 args = [cl.GetUpstreamBranch()]
4923
4924 if options.contributor:
4925 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4926 print("Please provide contibutor as 'First Last <email@example.com>'")
4927 return 1
4928
4929 base_branch = args[0]
4930
4931 if git_common.is_dirty_git_tree('land'):
4932 return 1
4933
4934 # This rev-list syntax means "show all commits not in my branch that
4935 # are in base_branch".
4936 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4937 base_branch]).splitlines()
4938 if upstream_commits:
4939 print('Base branch "%s" has %d commits '
4940 'not in this branch.' % (base_branch, len(upstream_commits)))
4941 print('Run "git merge %s" before attempting to land.' % base_branch)
4942 return 1
4943
4944 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4945 if not options.bypass_hooks:
4946 author = None
4947 if options.contributor:
4948 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4949 hook_results = cl.RunHook(
4950 committing=True,
4951 may_prompt=not options.force,
4952 verbose=options.verbose,
4953 change=cl.GetChange(merge_base, author))
4954 if not hook_results.should_continue():
4955 return 1
4956
4957 # Check the tree status if the tree status URL is set.
4958 status = GetTreeStatus()
4959 if 'closed' == status:
4960 print('The tree is closed. Please wait for it to reopen. Use '
4961 '"git cl land --bypass-hooks" to commit on a closed tree.')
4962 return 1
4963 elif 'unknown' == status:
4964 print('Unable to determine tree status. Please verify manually and '
4965 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4966 return 1
4967
4968 change_desc = ChangeDescription(options.message)
4969 if not change_desc.description and cl.GetIssue():
4970 change_desc = ChangeDescription(cl.GetDescription())
4971
4972 if not change_desc.description:
4973 if not cl.GetIssue() and options.bypass_hooks:
4974 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4975 else:
4976 print('No description set.')
4977 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4978 return 1
4979
4980 # Keep a separate copy for the commit message, because the commit message
4981 # contains the link to the Rietveld issue, while the Rietveld message contains
4982 # the commit viewvc url.
4983 if cl.GetIssue():
Aaron Gablea1bab272017-04-11 16:38:18 -07004984 change_desc.update_reviewers(
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004985 get_approving_reviewers(cl.GetIssueProperties()), [])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004986
4987 commit_desc = ChangeDescription(change_desc.description)
4988 if cl.GetIssue():
4989 # Xcode won't linkify this URL unless there is a non-whitespace character
4990 # after it. Add a period on a new line to circumvent this. Also add a space
4991 # before the period to make sure that Gitiles continues to correctly resolve
4992 # the URL.
4993 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4994 if options.contributor:
4995 commit_desc.append_footer('Patch from %s.' % options.contributor)
4996
4997 print('Description:')
4998 print(commit_desc.description)
4999
5000 branches = [merge_base, cl.GetBranchRef()]
5001 if not options.force:
5002 print_stats(options.similarity, options.find_copies, branches)
5003
5004 # We want to squash all this branch's commits into one commit with the proper
5005 # description. We do this by doing a "reset --soft" to the base branch (which
5006 # keeps the working copy the same), then landing that.
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005007 # Delete the special branches if they exist.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005008 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
5009 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
5010 result = RunGitWithCode(showref_cmd)
5011 if result[0] == 0:
5012 RunGit(['branch', '-D', branch])
5013
5014 # We might be in a directory that's present in this branch but not in the
5015 # trunk. Move up to the top of the tree so that git commands that expect a
5016 # valid CWD won't fail after we check out the merge branch.
5017 rel_base_path = settings.GetRelativeRoot()
5018 if rel_base_path:
5019 os.chdir(rel_base_path)
5020
5021 # Stuff our change into the merge branch.
5022 # We wrap in a try...finally block so if anything goes wrong,
5023 # we clean up the branches.
5024 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005025 revision = None
5026 try:
5027 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
5028 RunGit(['reset', '--soft', merge_base])
5029 if options.contributor:
5030 RunGit(
5031 [
5032 'commit', '--author', options.contributor,
5033 '-m', commit_desc.description,
5034 ])
5035 else:
5036 RunGit(['commit', '-m', commit_desc.description])
5037
5038 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
5039 mirror = settings.GetGitMirror(remote)
5040 if mirror:
5041 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005042 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005043 else:
5044 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005045 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005046 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
5047
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005048 retcode = PushToGitWithAutoRebase(
5049 pushurl, branch, commit_desc.description, git_numberer_enabled)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005050 if retcode == 0:
5051 revision = RunGit(['rev-parse', 'HEAD']).strip()
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005052 if git_numberer_enabled:
5053 change_desc = ChangeDescription(
5054 RunGit(['show', '-s', '--format=%B', 'HEAD']).strip())
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005055 except: # pylint: disable=bare-except
5056 if _IS_BEING_TESTED:
5057 logging.exception('this is likely your ACTUAL cause of test failure.\n'
5058 + '-' * 30 + '8<' + '-' * 30)
5059 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
5060 raise
5061 finally:
5062 # And then swap back to the original branch and clean up.
5063 RunGit(['checkout', '-q', cl.GetBranch()])
5064 RunGit(['branch', '-D', MERGE_BRANCH])
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005065 RunGit(['branch', '-D', CHERRY_PICK_BRANCH], error_ok=True)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005066
5067 if not revision:
5068 print('Failed to push. If this persists, please file a bug.')
5069 return 1
5070
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005071 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005072 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005073 if viewvc_url and revision:
5074 change_desc.append_footer(
5075 'Committed: %s%s' % (viewvc_url, revision))
5076 elif revision:
5077 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005078 print('Closing issue '
5079 '(you may be prompted for your codereview password)...')
5080 cl.UpdateDescription(change_desc.description)
5081 cl.CloseIssue()
5082 props = cl.GetIssueProperties()
5083 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005084 comment = "Committed patchset #%d (id:%d) manually as %s" % (
5085 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005086 if options.bypass_hooks:
5087 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
5088 else:
5089 comment += ' (presubmit successful).'
5090 cl.RpcServer().add_comment(cl.GetIssue(), comment)
5091
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005092 if os.path.isfile(POSTUPSTREAM_HOOK):
5093 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
5094
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005095 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005096
5097
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005098def PushToGitWithAutoRebase(remote, branch, original_description,
5099 git_numberer_enabled, max_attempts=3):
5100 """Pushes current HEAD commit on top of remote's branch.
5101
5102 Attempts to fetch and autorebase on push failures.
5103 Adds git number footers on the fly.
5104
5105 Returns integer code from last command.
5106 """
5107 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5108 code = 0
5109 attempts_left = max_attempts
5110 while attempts_left:
5111 attempts_left -= 1
5112 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5113
5114 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5115 # If fetch fails, retry.
5116 print('Fetching %s/%s...' % (remote, branch))
5117 code, out = RunGitWithCode(
5118 ['retry', 'fetch', remote,
5119 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5120 if code:
5121 print('Fetch failed with exit code %d.' % code)
5122 print(out.strip())
5123 continue
5124
5125 print('Cherry-picking commit on top of latest %s' % branch)
5126 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5127 suppress_stderr=True)
5128 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5129 code, out = RunGitWithCode(['cherry-pick', cherry])
5130 if code:
5131 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5132 'the following files have merge conflicts:' %
5133 (branch, parent_hash))
5134 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
5135 print('Please rebase your patch and try again.')
5136 RunGitWithCode(['cherry-pick', '--abort'])
5137 break
5138
5139 commit_desc = ChangeDescription(original_description)
5140 if git_numberer_enabled:
5141 logging.debug('Adding git number footers')
5142 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5143 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5144 branch)
5145 # Ensure timestamps are monotonically increasing.
5146 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5147 _get_committer_timestamp('HEAD'))
5148 _git_amend_head(commit_desc.description, timestamp)
5149
5150 code, out = RunGitWithCode(
5151 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5152 print(out)
5153 if code == 0:
5154 break
5155 if IsFatalPushFailure(out):
5156 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005157 'user.email are correct and you have push access to the repo.\n'
5158 'Hint: run command below to diangose common Git/Gerrit credential '
5159 'problems:\n'
5160 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005161 break
5162 return code
5163
5164
5165def IsFatalPushFailure(push_stdout):
5166 """True if retrying push won't help."""
5167 return '(prohibited by Gerrit)' in push_stdout
5168
5169
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005170@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005171def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005172 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005173 parser.add_option('-b', dest='newbranch',
5174 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005175 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005176 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005177 parser.add_option('-d', '--directory', action='store', metavar='DIR',
5178 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005179 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005180 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005181 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005182 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005183 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005184 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005185
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005186
5187 group = optparse.OptionGroup(
5188 parser,
5189 'Options for continuing work on the current issue uploaded from a '
5190 'different clone (e.g. different machine). Must be used independently '
5191 'from the other options. No issue number should be specified, and the '
5192 'branch must have an issue number associated with it')
5193 group.add_option('--reapply', action='store_true', dest='reapply',
5194 help='Reset the branch and reapply the issue.\n'
5195 'CAUTION: This will undo any local changes in this '
5196 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005197
5198 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005199 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005200 parser.add_option_group(group)
5201
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005202 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005203 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005204 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005205 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005206 auth_config = auth.extract_auth_config_from_options(options)
5207
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005208 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005209 if options.newbranch:
5210 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005211 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005212 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005213
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005214 cl = Changelist(auth_config=auth_config,
5215 codereview=options.forced_codereview)
5216 if not cl.GetIssue():
5217 parser.error('current branch must have an associated issue')
5218
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005219 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005220 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005221 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005222
5223 RunGit(['reset', '--hard', upstream])
5224 if options.pull:
5225 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005226
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005227 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5228 options.directory)
5229
5230 if len(args) != 1 or not args[0]:
5231 parser.error('Must specify issue number or url')
5232
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005233 target_issue_arg = ParseIssueNumberArgument(args[0],
5234 options.forced_codereview)
5235 if not target_issue_arg.valid:
5236 parser.error('invalid codereview url or CL id')
5237
5238 cl_kwargs = {
5239 'auth_config': auth_config,
5240 'codereview_host': target_issue_arg.hostname,
5241 'codereview': options.forced_codereview,
5242 }
5243 detected_codereview_from_url = False
5244 if target_issue_arg.codereview and not options.forced_codereview:
5245 detected_codereview_from_url = True
5246 cl_kwargs['codereview'] = target_issue_arg.codereview
5247 cl_kwargs['issue'] = target_issue_arg.issue
5248
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005249 # We don't want uncommitted changes mixed up with the patch.
5250 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005251 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005252
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005253 if options.newbranch:
5254 if options.force:
5255 RunGit(['branch', '-D', options.newbranch],
5256 stderr=subprocess2.PIPE, error_ok=True)
5257 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07005258 elif not GetCurrentBranch():
5259 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005260
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005261 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005262
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005263 if cl.IsGerrit():
5264 if options.reject:
5265 parser.error('--reject is not supported with Gerrit codereview.')
5266 if options.nocommit:
5267 parser.error('--nocommit is not supported with Gerrit codereview.')
5268 if options.directory:
5269 parser.error('--directory is not supported with Gerrit codereview.')
5270
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005271 if detected_codereview_from_url:
5272 print('canonical issue/change URL: %s (type: %s)\n' %
5273 (cl.GetIssueURL(), target_issue_arg.codereview))
5274
5275 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
5276 options.nocommit, options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005277
5278
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005279def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005280 """Fetches the tree status and returns either 'open', 'closed',
5281 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005282 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005283 if url:
5284 status = urllib2.urlopen(url).read().lower()
5285 if status.find('closed') != -1 or status == '0':
5286 return 'closed'
5287 elif status.find('open') != -1 or status == '1':
5288 return 'open'
5289 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005290 return 'unset'
5291
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005292
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005293def GetTreeStatusReason():
5294 """Fetches the tree status from a json url and returns the message
5295 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005296 url = settings.GetTreeStatusUrl()
5297 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005298 connection = urllib2.urlopen(json_url)
5299 status = json.loads(connection.read())
5300 connection.close()
5301 return status['message']
5302
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005303
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005304def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005305 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005306 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005307 status = GetTreeStatus()
5308 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005309 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005310 return 2
5311
vapiera7fbd5a2016-06-16 09:17:49 -07005312 print('The tree is %s' % status)
5313 print()
5314 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005315 if status != 'open':
5316 return 1
5317 return 0
5318
5319
maruel@chromium.org15192402012-09-06 12:38:29 +00005320def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005321 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005322 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005323 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005324 '-b', '--bot', action='append',
5325 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5326 'times to specify multiple builders. ex: '
5327 '"-b win_rel -b win_layout". See '
5328 'the try server waterfall for the builders name and the tests '
5329 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005330 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005331 '-B', '--bucket', default='',
5332 help=('Buildbucket bucket to send the try requests.'))
5333 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005334 '-m', '--master', default='',
5335 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005336 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005337 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005338 help='Revision to use for the try job; default: the revision will '
5339 'be determined by the try recipe that builder runs, which usually '
5340 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005341 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005342 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005343 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005344 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005345 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005346 '--project',
5347 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005348 'in recipe to determine to which repository or directory to '
5349 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005350 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005351 '-p', '--property', dest='properties', action='append', default=[],
5352 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005353 'key2=value2 etc. The value will be treated as '
5354 'json if decodable, or as string otherwise. '
5355 'NOTE: using this may make your try job not usable for CQ, '
5356 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005357 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005358 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5359 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005360 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005361 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005362 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005363 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005364
machenbach@chromium.org45453142015-09-15 08:45:22 +00005365 # Make sure that all properties are prop=value pairs.
5366 bad_params = [x for x in options.properties if '=' not in x]
5367 if bad_params:
5368 parser.error('Got properties with missing "=": %s' % bad_params)
5369
maruel@chromium.org15192402012-09-06 12:38:29 +00005370 if args:
5371 parser.error('Unknown arguments: %s' % args)
5372
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005373 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00005374 if not cl.GetIssue():
5375 parser.error('Need to upload first')
5376
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005377 if cl.IsGerrit():
5378 # HACK: warm up Gerrit change detail cache to save on RPCs.
5379 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5380
tandriie113dfd2016-10-11 10:20:12 -07005381 error_message = cl.CannotTriggerTryJobReason()
5382 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005383 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005384
borenet6c0efe62016-10-19 08:13:29 -07005385 if options.bucket and options.master:
5386 parser.error('Only one of --bucket and --master may be used.')
5387
qyearsley1fdfcb62016-10-24 13:22:03 -07005388 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005389
qyearsleydd49f942016-10-28 11:57:22 -07005390 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5391 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005392 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005393 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005394 print('git cl try with no bots now defaults to CQ dry run.')
5395 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5396 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005397
borenet6c0efe62016-10-19 08:13:29 -07005398 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005399 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005400 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005401 'of bot requires an initial job from a parent (usually a builder). '
5402 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005403 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005404 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005405
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005406 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005407 # TODO(tandrii): Checking local patchset against remote patchset is only
5408 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5409 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005410 print('Warning: Codereview server has newer patchsets (%s) than most '
5411 'recent upload from local checkout (%s). Did a previous upload '
5412 'fail?\n'
5413 'By default, git cl try uses the latest patchset from '
5414 'codereview, continuing to use patchset %s.\n' %
5415 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005416
tandrii568043b2016-10-11 07:49:18 -07005417 try:
borenet6c0efe62016-10-19 08:13:29 -07005418 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5419 patchset)
tandrii568043b2016-10-11 07:49:18 -07005420 except BuildbucketResponseException as ex:
5421 print('ERROR: %s' % ex)
5422 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005423 return 0
5424
5425
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005426def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005427 """Prints info about try jobs associated with current CL."""
5428 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005429 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005430 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005431 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005432 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005433 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005434 '--color', action='store_true', default=setup_color.IS_TTY,
5435 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005436 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005437 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5438 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005439 group.add_option(
5440 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005441 parser.add_option_group(group)
5442 auth.add_auth_options(parser)
5443 options, args = parser.parse_args(args)
5444 if args:
5445 parser.error('Unrecognized args: %s' % ' '.join(args))
5446
5447 auth_config = auth.extract_auth_config_from_options(options)
5448 cl = Changelist(auth_config=auth_config)
5449 if not cl.GetIssue():
5450 parser.error('Need to upload first')
5451
tandrii221ab252016-10-06 08:12:04 -07005452 patchset = options.patchset
5453 if not patchset:
5454 patchset = cl.GetMostRecentPatchset()
5455 if not patchset:
5456 parser.error('Codereview doesn\'t know about issue %s. '
5457 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005458 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005459 cl.GetIssue())
5460
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005461 # TODO(tandrii): Checking local patchset against remote patchset is only
5462 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5463 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005464 print('Warning: Codereview server has newer patchsets (%s) than most '
5465 'recent upload from local checkout (%s). Did a previous upload '
5466 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005467 'By default, git cl try-results uses the latest patchset from '
5468 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005469 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005470 try:
tandrii221ab252016-10-06 08:12:04 -07005471 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005472 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005473 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005474 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005475 if options.json:
5476 write_try_results_json(options.json, jobs)
5477 else:
5478 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005479 return 0
5480
5481
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005482@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005483def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005484 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005485 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005486 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005487 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005488
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005489 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005490 if args:
5491 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005492 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005493 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005494 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005495 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005496
5497 # Clear configured merge-base, if there is one.
5498 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005499 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005500 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005501 return 0
5502
5503
thestig@chromium.org00858c82013-12-02 23:08:03 +00005504def CMDweb(parser, args):
5505 """Opens the current CL in the web browser."""
5506 _, args = parser.parse_args(args)
5507 if args:
5508 parser.error('Unrecognized args: %s' % ' '.join(args))
5509
5510 issue_url = Changelist().GetIssueURL()
5511 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005512 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005513 return 1
5514
5515 webbrowser.open(issue_url)
5516 return 0
5517
5518
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005519def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005520 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005521 parser.add_option('-d', '--dry-run', action='store_true',
5522 help='trigger in dry run mode')
5523 parser.add_option('-c', '--clear', action='store_true',
5524 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005525 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005526 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005527 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005528 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005529 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005530 if args:
5531 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005532 if options.dry_run and options.clear:
5533 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5534
iannuccie53c9352016-08-17 14:40:40 -07005535 cl = Changelist(auth_config=auth_config, issue=options.issue,
5536 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005537 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005538 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005539 elif options.dry_run:
5540 state = _CQState.DRY_RUN
5541 else:
5542 state = _CQState.COMMIT
5543 if not cl.GetIssue():
5544 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005545 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005546 return 0
5547
5548
groby@chromium.org411034a2013-02-26 15:12:01 +00005549def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005550 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005551 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005552 auth.add_auth_options(parser)
5553 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005554 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005555 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005556 if args:
5557 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005558 cl = Changelist(auth_config=auth_config, issue=options.issue,
5559 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005560 # Ensure there actually is an issue to close.
5561 cl.GetDescription()
5562 cl.CloseIssue()
5563 return 0
5564
5565
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005566def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005567 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005568 parser.add_option(
5569 '--stat',
5570 action='store_true',
5571 dest='stat',
5572 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005573 auth.add_auth_options(parser)
5574 options, args = parser.parse_args(args)
5575 auth_config = auth.extract_auth_config_from_options(options)
5576 if args:
5577 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005578
5579 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005580 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005581 # Staged changes would be committed along with the patch from last
5582 # upload, hence counted toward the "last upload" side in the final
5583 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005584 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005585 return 1
5586
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005587 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005588 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005589 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005590 if not issue:
5591 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005592 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005593 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005594
5595 # Create a new branch based on the merge-base
5596 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005597 # Clear cached branch in cl object, to avoid overwriting original CL branch
5598 # properties.
5599 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005600 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005601 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005602 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005603 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005604 return rtn
5605
wychen@chromium.org06928532015-02-03 02:11:29 +00005606 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005607 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005608 cmd = ['git', 'diff']
5609 if options.stat:
5610 cmd.append('--stat')
5611 cmd.extend([TMP_BRANCH, branch, '--'])
5612 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005613 finally:
5614 RunGit(['checkout', '-q', branch])
5615 RunGit(['branch', '-D', TMP_BRANCH])
5616
5617 return 0
5618
5619
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005620def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005621 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005622 parser.add_option(
5623 '--no-color',
5624 action='store_true',
5625 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005626 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005627 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005628 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005629
5630 author = RunGit(['config', 'user.email']).strip() or None
5631
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005632 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005633
5634 if args:
5635 if len(args) > 1:
5636 parser.error('Unknown args')
5637 base_branch = args[0]
5638 else:
5639 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005640 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005641
5642 change = cl.GetChange(base_branch, None)
5643 return owners_finder.OwnersFinder(
5644 [f.LocalPath() for f in
5645 cl.GetChange(base_branch, None).AffectedFiles()],
Jochen Eisinger72606f82017-04-04 10:44:18 +02005646 change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02005647 author, fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005648 disable_color=options.no_color,
5649 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005650
5651
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005652def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005653 """Generates a diff command."""
5654 # Generate diff for the current branch's changes.
5655 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005656 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005657
5658 if args:
5659 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005660 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005661 diff_cmd.append(arg)
5662 else:
5663 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005664
5665 return diff_cmd
5666
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005667
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005668def MatchingFileType(file_name, extensions):
5669 """Returns true if the file name ends with one of the given extensions."""
5670 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005671
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005672
enne@chromium.org555cfe42014-01-29 18:21:39 +00005673@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005674def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005675 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005676 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005677 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005678 parser.add_option('--full', action='store_true',
5679 help='Reformat the full content of all touched files')
5680 parser.add_option('--dry-run', action='store_true',
5681 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005682 parser.add_option('--python', action='store_true',
5683 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005684 parser.add_option('--js', action='store_true',
5685 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005686 parser.add_option('--diff', action='store_true',
5687 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005688 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005689
Daniel Chengc55eecf2016-12-30 03:11:02 -08005690 # Normalize any remaining args against the current path, so paths relative to
5691 # the current directory are still resolved as expected.
5692 args = [os.path.join(os.getcwd(), arg) for arg in args]
5693
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005694 # git diff generates paths against the root of the repository. Change
5695 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005696 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005697 if rel_base_path:
5698 os.chdir(rel_base_path)
5699
digit@chromium.org29e47272013-05-17 17:01:46 +00005700 # Grab the merge-base commit, i.e. the upstream commit of the current
5701 # branch when it was created or the last time it was rebased. This is
5702 # to cover the case where the user may have called "git fetch origin",
5703 # moving the origin branch to a newer commit, but hasn't rebased yet.
5704 upstream_commit = None
5705 cl = Changelist()
5706 upstream_branch = cl.GetUpstreamBranch()
5707 if upstream_branch:
5708 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5709 upstream_commit = upstream_commit.strip()
5710
5711 if not upstream_commit:
5712 DieWithError('Could not find base commit for this branch. '
5713 'Are you in detached state?')
5714
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005715 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5716 diff_output = RunGit(changed_files_cmd)
5717 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005718 # Filter out files deleted by this CL
5719 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005720
Christopher Lamc5ba6922017-01-24 11:19:14 +11005721 if opts.js:
5722 CLANG_EXTS.append('.js')
5723
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005724 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5725 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5726 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005727 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005728
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005729 top_dir = os.path.normpath(
5730 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5731
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005732 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5733 # formatted. This is used to block during the presubmit.
5734 return_value = 0
5735
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005736 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005737 # Locate the clang-format binary in the checkout
5738 try:
5739 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005740 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005741 DieWithError(e)
5742
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005743 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005744 cmd = [clang_format_tool]
5745 if not opts.dry_run and not opts.diff:
5746 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005747 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005748 if opts.diff:
5749 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005750 else:
5751 env = os.environ.copy()
5752 env['PATH'] = str(os.path.dirname(clang_format_tool))
5753 try:
5754 script = clang_format.FindClangFormatScriptInChromiumTree(
5755 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005756 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005757 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005758
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005759 cmd = [sys.executable, script, '-p0']
5760 if not opts.dry_run and not opts.diff:
5761 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005762
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005763 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5764 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005765
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005766 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5767 if opts.diff:
5768 sys.stdout.write(stdout)
5769 if opts.dry_run and len(stdout) > 0:
5770 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005771
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005772 # Similar code to above, but using yapf on .py files rather than clang-format
5773 # on C/C++ files
5774 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005775 yapf_tool = gclient_utils.FindExecutable('yapf')
5776 if yapf_tool is None:
5777 DieWithError('yapf not found in PATH')
5778
5779 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005780 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005781 cmd = [yapf_tool]
5782 if not opts.dry_run and not opts.diff:
5783 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005784 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005785 if opts.diff:
5786 sys.stdout.write(stdout)
5787 else:
5788 # TODO(sbc): yapf --lines mode still has some issues.
5789 # https://github.com/google/yapf/issues/154
5790 DieWithError('--python currently only works with --full')
5791
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005792 # Dart's formatter does not have the nice property of only operating on
5793 # modified chunks, so hard code full.
5794 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005795 try:
5796 command = [dart_format.FindDartFmtToolInChromiumTree()]
5797 if not opts.dry_run and not opts.diff:
5798 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005799 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005800
ppi@chromium.org6593d932016-03-03 15:41:15 +00005801 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005802 if opts.dry_run and stdout:
5803 return_value = 2
5804 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005805 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5806 'found in this checkout. Files in other languages are still '
5807 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005808
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005809 # Format GN build files. Always run on full build files for canonical form.
5810 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005811 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005812 if opts.dry_run or opts.diff:
5813 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005814 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005815 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5816 shell=sys.platform == 'win32',
5817 cwd=top_dir)
5818 if opts.dry_run and gn_ret == 2:
5819 return_value = 2 # Not formatted.
5820 elif opts.diff and gn_ret == 2:
5821 # TODO this should compute and print the actual diff.
5822 print("This change has GN build file diff for " + gn_diff_file)
5823 elif gn_ret != 0:
5824 # For non-dry run cases (and non-2 return values for dry-run), a
5825 # nonzero error code indicates a failure, probably because the file
5826 # doesn't parse.
5827 DieWithError("gn format failed on " + gn_diff_file +
5828 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005829
Steven Holte2e664bf2017-04-21 13:10:47 -07005830 for xml_dir in GetDirtyMetricsDirs(diff_files):
5831 tool_dir = os.path.join(top_dir, xml_dir)
5832 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5833 if opts.dry_run or opts.diff:
5834 cmd.append('--diff')
5835 stdout = RunCommand(cmd, cwd=top_dir)
5836 if opts.diff:
5837 sys.stdout.write(stdout)
5838 if opts.dry_run and stdout:
5839 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005840
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005841 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005842
Steven Holte2e664bf2017-04-21 13:10:47 -07005843def GetDirtyMetricsDirs(diff_files):
5844 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5845 metrics_xml_dirs = [
5846 os.path.join('tools', 'metrics', 'actions'),
5847 os.path.join('tools', 'metrics', 'histograms'),
5848 os.path.join('tools', 'metrics', 'rappor'),
5849 os.path.join('tools', 'metrics', 'ukm')]
5850 for xml_dir in metrics_xml_dirs:
5851 if any(file.startswith(xml_dir) for file in xml_diff_files):
5852 yield xml_dir
5853
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005854
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005855@subcommand.usage('<codereview url or issue id>')
5856def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005857 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005858 _, args = parser.parse_args(args)
5859
5860 if len(args) != 1:
5861 parser.print_help()
5862 return 1
5863
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005864 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005865 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005866 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005867
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005868 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005869
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005870 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005871 output = RunGit(['config', '--local', '--get-regexp',
5872 r'branch\..*\.%s' % issueprefix],
5873 error_ok=True)
5874 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005875 if issue == target_issue:
5876 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005877
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005878 branches = []
5879 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005880 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005881 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005882 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005883 return 1
5884 if len(branches) == 1:
5885 RunGit(['checkout', branches[0]])
5886 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005887 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005888 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005889 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005890 which = raw_input('Choose by index: ')
5891 try:
5892 RunGit(['checkout', branches[int(which)]])
5893 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005894 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005895 return 1
5896
5897 return 0
5898
5899
maruel@chromium.org29404b52014-09-08 22:58:00 +00005900def CMDlol(parser, args):
5901 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005902 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005903 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5904 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5905 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005906 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005907 return 0
5908
5909
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005910class OptionParser(optparse.OptionParser):
5911 """Creates the option parse and add --verbose support."""
5912 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005913 optparse.OptionParser.__init__(
5914 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005915 self.add_option(
5916 '-v', '--verbose', action='count', default=0,
5917 help='Use 2 times for more debugging info')
5918
5919 def parse_args(self, args=None, values=None):
5920 options, args = optparse.OptionParser.parse_args(self, args, values)
5921 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005922 logging.basicConfig(
5923 level=levels[min(options.verbose, len(levels) - 1)],
5924 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5925 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005926 return options, args
5927
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005928
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005929def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005930 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005931 print('\nYour python version %s is unsupported, please upgrade.\n' %
5932 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005933 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005934
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005935 # Reload settings.
5936 global settings
5937 settings = Settings()
5938
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005939 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005940 dispatcher = subcommand.CommandDispatcher(__name__)
5941 try:
5942 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005943 except auth.AuthenticationError as e:
5944 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005945 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005946 if e.code != 500:
5947 raise
5948 DieWithError(
5949 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5950 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005951 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005952
5953
5954if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005955 # These affect sys.stdout so do it outside of main() to simplify mocks in
5956 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005957 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005958 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005959 try:
5960 sys.exit(main(sys.argv[1:]))
5961 except KeyboardInterrupt:
5962 sys.stderr.write('interrupted\n')
5963 sys.exit(1)