blob: 4171243c696d6635499adf862d5fb79bd342bf54 [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
maruel@chromium.org0633fb42013-08-16 20:06:14 +000062import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000063import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064import watchlists
65
tandrii7400cf02016-06-21 08:48:07 -070066__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067
tandrii9d2c7a32016-06-22 03:42:45 -070068COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070069DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080070POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000072REFS_THAT_ALIAS_TO_OTHER_REFS = {
73 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
74 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
75}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
thestig@chromium.org44202a22014-03-11 19:22:18 +000077# Valid extensions for files we want to lint.
78DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
79DEFAULT_LINT_IGNORE_REGEX = r"$^"
80
borenet6c0efe62016-10-19 08:13:29 -070081# Buildbucket master name prefix.
82MASTER_PREFIX = 'master.'
83
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000084# Shortcut since it quickly becomes redundant.
85Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000086
maruel@chromium.orgddd59412011-11-30 14:20:38 +000087# Initialized in main()
88settings = None
89
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010090# Used by tests/git_cl_test.py to add extra logging.
91# Inside the weirdly failing test, add this:
92# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
93# And scroll up to see the strack trace printed.
94_IS_BEING_TESTED = False
95
maruel@chromium.orgddd59412011-11-30 14:20:38 +000096
Christopher Lamf732cd52017-01-24 12:40:11 +110097def DieWithError(message, change_desc=None):
98 if change_desc:
99 SaveDescriptionBackup(change_desc)
100
vapiera7fbd5a2016-06-16 09:17:49 -0700101 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 sys.exit(1)
103
104
Christopher Lamf732cd52017-01-24 12:40:11 +1100105def SaveDescriptionBackup(change_desc):
106 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
107 print('\nError after CL description prompt -- saving description to %s\n' %
108 backup_path)
109 backup_file = open(backup_path, 'w')
110 backup_file.write(change_desc.description)
111 backup_file.close()
112
113
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000114def GetNoGitPagerEnv():
115 env = os.environ.copy()
116 # 'cat' is a magical git string that disables pagers on all platforms.
117 env['GIT_PAGER'] = 'cat'
118 return env
119
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000120
bsep@chromium.org627d9002016-04-29 00:00:52 +0000121def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000122 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000123 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000124 except subprocess2.CalledProcessError as e:
125 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000126 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000127 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000128 'Command "%s" failed.\n%s' % (
129 ' '.join(args), error_message or e.stdout or ''))
130 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
133def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000134 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000135 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000136
137
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000138def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000139 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700140 if suppress_stderr:
141 stderr = subprocess2.VOID
142 else:
143 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000144 try:
tandrii5d48c322016-08-18 16:19:37 -0700145 (out, _), code = subprocess2.communicate(['git'] + args,
146 env=GetNoGitPagerEnv(),
147 stdout=subprocess2.PIPE,
148 stderr=stderr)
149 return code, out
150 except subprocess2.CalledProcessError as e:
151 logging.debug('Failed running %s', args)
152 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000153
154
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000155def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000156 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000157 return RunGitWithCode(args, suppress_stderr=True)[1]
158
159
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000160def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000161 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000163 return (version.startswith(prefix) and
164 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000165
166
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000167def BranchExists(branch):
168 """Return True if specified branch exists."""
169 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
170 suppress_stderr=True)
171 return not code
172
173
tandrii2a16b952016-10-19 07:09:44 -0700174def time_sleep(seconds):
175 # Use this so that it can be mocked in tests without interfering with python
176 # system machinery.
177 import time # Local import to discourage others from importing time globally.
178 return time.sleep(seconds)
179
180
maruel@chromium.org90541732011-04-01 17:54:18 +0000181def ask_for_data(prompt):
182 try:
183 return raw_input(prompt)
184 except KeyboardInterrupt:
185 # Hide the exception.
186 sys.exit(1)
187
188
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100189def confirm_or_exit(prefix='', action='confirm'):
190 """Asks user to press enter to continue or press Ctrl+C to abort."""
191 if not prefix or prefix.endswith('\n'):
192 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100193 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100194 mid = ' Press'
195 elif prefix.endswith(' '):
196 mid = 'press'
197 else:
198 mid = ' press'
199 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
200
201
202def ask_for_explicit_yes(prompt):
203 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
204 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
205 while True:
206 if 'yes'.startswith(result):
207 return True
208 if 'no'.startswith(result):
209 return False
210 result = ask_for_data('Please, type yes or no: ').lower()
211
212
tandrii5d48c322016-08-18 16:19:37 -0700213def _git_branch_config_key(branch, key):
214 """Helper method to return Git config key for a branch."""
215 assert branch, 'branch name is required to set git config for it'
216 return 'branch.%s.%s' % (branch, key)
217
218
219def _git_get_branch_config_value(key, default=None, value_type=str,
220 branch=False):
221 """Returns git config value of given or current branch if any.
222
223 Returns default in all other cases.
224 """
225 assert value_type in (int, str, bool)
226 if branch is False: # Distinguishing default arg value from None.
227 branch = GetCurrentBranch()
228
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000229 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700230 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000231
tandrii5d48c322016-08-18 16:19:37 -0700232 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700233 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700234 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700235 # git config also has --int, but apparently git config suffers from integer
236 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700237 args.append(_git_branch_config_key(branch, key))
238 code, out = RunGitWithCode(args)
239 if code == 0:
240 value = out.strip()
241 if value_type == int:
242 return int(value)
243 if value_type == bool:
244 return bool(value.lower() == 'true')
245 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000246 return default
247
248
tandrii5d48c322016-08-18 16:19:37 -0700249def _git_set_branch_config_value(key, value, branch=None, **kwargs):
250 """Sets the value or unsets if it's None of a git branch config.
251
252 Valid, though not necessarily existing, branch must be provided,
253 otherwise currently checked out branch is used.
254 """
255 if not branch:
256 branch = GetCurrentBranch()
257 assert branch, 'a branch name OR currently checked out branch is required'
258 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700259 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700260 if value is None:
261 args.append('--unset')
262 elif isinstance(value, bool):
263 args.append('--bool')
264 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700265 else:
tandrii33a46ff2016-08-23 05:53:40 -0700266 # git config also has --int, but apparently git config suffers from integer
267 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700268 value = str(value)
269 args.append(_git_branch_config_key(branch, key))
270 if value is not None:
271 args.append(value)
272 RunGit(args, **kwargs)
273
274
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100275def _get_committer_timestamp(commit):
276 """Returns unix timestamp as integer of a committer in a commit.
277
278 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
279 """
280 # Git also stores timezone offset, but it only affects visual display,
281 # actual point in time is defined by this timestamp only.
282 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
283
284
285def _git_amend_head(message, committer_timestamp):
286 """Amends commit with new message and desired committer_timestamp.
287
288 Sets committer timezone to UTC.
289 """
290 env = os.environ.copy()
291 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
292 return RunGit(['commit', '--amend', '-m', message], env=env)
293
294
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000295def add_git_similarity(parser):
296 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700297 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000298 help='Sets the percentage that a pair of files need to match in order to'
299 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000300 parser.add_option(
301 '--find-copies', action='store_true',
302 help='Allows git to look for copies.')
303 parser.add_option(
304 '--no-find-copies', action='store_false', dest='find_copies',
305 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000306
307 old_parser_args = parser.parse_args
308 def Parse(args):
309 options, args = old_parser_args(args)
310
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000311 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700312 options.similarity = _git_get_branch_config_value(
313 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000314 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000315 print('Note: Saving similarity of %d%% in git config.'
316 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700317 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000318
iannucci@chromium.org79540052012-10-19 23:15:26 +0000319 options.similarity = max(0, min(options.similarity, 100))
320
321 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700322 options.find_copies = _git_get_branch_config_value(
323 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000324 else:
tandrii5d48c322016-08-18 16:19:37 -0700325 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000326
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000327 return options, args
328 parser.parse_args = Parse
329
330
machenbach@chromium.org45453142015-09-15 08:45:22 +0000331def _get_properties_from_options(options):
332 properties = dict(x.split('=', 1) for x in options.properties)
333 for key, val in properties.iteritems():
334 try:
335 properties[key] = json.loads(val)
336 except ValueError:
337 pass # If a value couldn't be evaluated, treat it as a string.
338 return properties
339
340
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000341def _prefix_master(master):
342 """Convert user-specified master name to full master name.
343
344 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
345 name, while the developers always use shortened master name
346 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
347 function does the conversion for buildbucket migration.
348 """
borenet6c0efe62016-10-19 08:13:29 -0700349 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000350 return master
borenet6c0efe62016-10-19 08:13:29 -0700351 return '%s%s' % (MASTER_PREFIX, master)
352
353
354def _unprefix_master(bucket):
355 """Convert bucket name to shortened master name.
356
357 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
358 name, while the developers always use shortened master name
359 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
360 function does the conversion for buildbucket migration.
361 """
362 if bucket.startswith(MASTER_PREFIX):
363 return bucket[len(MASTER_PREFIX):]
364 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000365
366
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000367def _buildbucket_retry(operation_name, http, *args, **kwargs):
368 """Retries requests to buildbucket service and returns parsed json content."""
369 try_count = 0
370 while True:
371 response, content = http.request(*args, **kwargs)
372 try:
373 content_json = json.loads(content)
374 except ValueError:
375 content_json = None
376
377 # Buildbucket could return an error even if status==200.
378 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000379 error = content_json.get('error')
380 if error.get('code') == 403:
381 raise BuildbucketResponseException(
382 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000383 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000384 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000385 raise BuildbucketResponseException(msg)
386
387 if response.status == 200:
388 if not content_json:
389 raise BuildbucketResponseException(
390 'Buildbucket returns invalid json content: %s.\n'
391 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
392 content)
393 return content_json
394 if response.status < 500 or try_count >= 2:
395 raise httplib2.HttpLib2Error(content)
396
397 # status >= 500 means transient failures.
398 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700399 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000400 try_count += 1
401 assert False, 'unreachable'
402
403
qyearsley1fdfcb62016-10-24 13:22:03 -0700404def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700405 """Returns a dict mapping bucket names to builders and tests,
406 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700407 """
qyearsleydd49f942016-10-28 11:57:22 -0700408 # If no bots are listed, we try to get a set of builders and tests based
409 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 if not options.bot:
411 change = changelist.GetChange(
412 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700413 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700414 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700415 change=change,
416 changed_files=change.LocalPaths(),
417 repository_root=settings.GetRoot(),
418 default_presubmit=None,
419 project=None,
420 verbose=options.verbose,
421 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700422 if masters is None:
423 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100424 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700425
qyearsley1fdfcb62016-10-24 13:22:03 -0700426 if options.bucket:
427 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700428 if options.master:
429 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700430
qyearsleydd49f942016-10-28 11:57:22 -0700431 # If bots are listed but no master or bucket, then we need to find out
432 # the corresponding master for each bot.
433 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
434 if error_message:
435 option_parser.error(
436 'Tryserver master cannot be found because: %s\n'
437 'Please manually specify the tryserver master, e.g. '
438 '"-m tryserver.chromium.linux".' % error_message)
439 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700440
441
qyearsley123a4682016-10-26 09:12:17 -0700442def _get_bucket_map_for_builders(builders):
443 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700444 map_url = 'https://builders-map.appspot.com/'
445 try:
qyearsley123a4682016-10-26 09:12:17 -0700446 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700447 except urllib2.URLError as e:
448 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
449 (map_url, e))
450 except ValueError as e:
451 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700452 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700453 return None, 'Failed to build master map.'
454
qyearsley123a4682016-10-26 09:12:17 -0700455 bucket_map = {}
456 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700457 masters = builders_map.get(builder, [])
458 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700459 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700460 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700461 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700462 (builder, masters))
463 bucket = _prefix_master(masters[0])
464 bucket_map.setdefault(bucket, {})[builder] = []
465
466 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700467
468
borenet6c0efe62016-10-19 08:13:29 -0700469def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700470 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700471 """Sends a request to Buildbucket to trigger try jobs for a changelist.
472
473 Args:
474 auth_config: AuthConfig for Rietveld.
475 changelist: Changelist that the try jobs are associated with.
476 buckets: A nested dict mapping bucket names to builders to tests.
477 options: Command-line options.
478 """
tandriide281ae2016-10-12 06:02:30 -0700479 assert changelist.GetIssue(), 'CL must be uploaded first'
480 codereview_url = changelist.GetCodereviewServer()
481 assert codereview_url, 'CL must be uploaded first'
482 patchset = patchset or changelist.GetMostRecentPatchset()
483 assert patchset, 'CL must be uploaded first'
484
485 codereview_host = urlparse.urlparse(codereview_url).hostname
486 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000487 http = authenticator.authorize(httplib2.Http())
488 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700489
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000490 buildbucket_put_url = (
491 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000492 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700493 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
494 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
495 hostname=codereview_host,
496 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000497 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700498
499 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
500 shared_parameters_properties['category'] = category
501 if options.clobber:
502 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700503 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700504 if extra_properties:
505 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000506
507 batch_req_body = {'builds': []}
508 print_text = []
509 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700510 for bucket, builders_and_tests in sorted(buckets.iteritems()):
511 print_text.append('Bucket: %s' % bucket)
512 master = None
513 if bucket.startswith(MASTER_PREFIX):
514 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000515 for builder, tests in sorted(builders_and_tests.iteritems()):
516 print_text.append(' %s: %s' % (builder, tests))
517 parameters = {
518 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000519 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100520 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000521 'revision': options.revision,
522 }],
tandrii8c5a3532016-11-04 07:52:02 -0700523 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000524 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000525 if 'presubmit' in builder.lower():
526 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000527 if tests:
528 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700529
530 tags = [
531 'builder:%s' % builder,
532 'buildset:%s' % buildset,
533 'user_agent:git_cl_try',
534 ]
535 if master:
536 parameters['properties']['master'] = master
537 tags.append('master:%s' % master)
538
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000539 batch_req_body['builds'].append(
540 {
541 'bucket': bucket,
542 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000543 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700544 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000545 }
546 )
547
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000548 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700549 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 http,
551 buildbucket_put_url,
552 'PUT',
553 body=json.dumps(batch_req_body),
554 headers={'Content-Type': 'application/json'}
555 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000556 print_text.append('To see results here, run: git cl try-results')
557 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700558 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000559
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000560
tandrii221ab252016-10-06 08:12:04 -0700561def fetch_try_jobs(auth_config, changelist, buildbucket_host,
562 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700563 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564
qyearsley53f48a12016-09-01 10:45:13 -0700565 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566 """
tandrii221ab252016-10-06 08:12:04 -0700567 assert buildbucket_host
568 assert changelist.GetIssue(), 'CL must be uploaded first'
569 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
570 patchset = patchset or changelist.GetMostRecentPatchset()
571 assert patchset, 'CL must be uploaded first'
572
573 codereview_url = changelist.GetCodereviewServer()
574 codereview_host = urlparse.urlparse(codereview_url).hostname
575 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576 if authenticator.has_cached_credentials():
577 http = authenticator.authorize(httplib2.Http())
578 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700579 print('Warning: Some results might be missing because %s' %
580 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700581 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000582 http = httplib2.Http()
583
584 http.force_exception_to_status_code = True
585
tandrii221ab252016-10-06 08:12:04 -0700586 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
587 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
588 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000589 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700590 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 params = {'tag': 'buildset:%s' % buildset}
592
593 builds = {}
594 while True:
595 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700596 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000597 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700598 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000599 for build in content.get('builds', []):
600 builds[build['id']] = build
601 if 'next_cursor' in content:
602 params['start_cursor'] = content['next_cursor']
603 else:
604 break
605 return builds
606
607
qyearsleyeab3c042016-08-24 09:18:28 -0700608def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000609 """Prints nicely result of fetch_try_jobs."""
610 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700611 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000612 return
613
614 # Make a copy, because we'll be modifying builds dictionary.
615 builds = builds.copy()
616 builder_names_cache = {}
617
618 def get_builder(b):
619 try:
620 return builder_names_cache[b['id']]
621 except KeyError:
622 try:
623 parameters = json.loads(b['parameters_json'])
624 name = parameters['builder_name']
625 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700626 print('WARNING: failed to get builder name for build %s: %s' % (
627 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000628 name = None
629 builder_names_cache[b['id']] = name
630 return name
631
632 def get_bucket(b):
633 bucket = b['bucket']
634 if bucket.startswith('master.'):
635 return bucket[len('master.'):]
636 return bucket
637
638 if options.print_master:
639 name_fmt = '%%-%ds %%-%ds' % (
640 max(len(str(get_bucket(b))) for b in builds.itervalues()),
641 max(len(str(get_builder(b))) for b in builds.itervalues()))
642 def get_name(b):
643 return name_fmt % (get_bucket(b), get_builder(b))
644 else:
645 name_fmt = '%%-%ds' % (
646 max(len(str(get_builder(b))) for b in builds.itervalues()))
647 def get_name(b):
648 return name_fmt % get_builder(b)
649
650 def sort_key(b):
651 return b['status'], b.get('result'), get_name(b), b.get('url')
652
653 def pop(title, f, color=None, **kwargs):
654 """Pop matching builds from `builds` dict and print them."""
655
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000656 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000657 colorize = str
658 else:
659 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
660
661 result = []
662 for b in builds.values():
663 if all(b.get(k) == v for k, v in kwargs.iteritems()):
664 builds.pop(b['id'])
665 result.append(b)
666 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700667 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000668 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700669 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000670
671 total = len(builds)
672 pop(status='COMPLETED', result='SUCCESS',
673 title='Successes:', color=Fore.GREEN,
674 f=lambda b: (get_name(b), b.get('url')))
675 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
676 title='Infra Failures:', color=Fore.MAGENTA,
677 f=lambda b: (get_name(b), b.get('url')))
678 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
679 title='Failures:', color=Fore.RED,
680 f=lambda b: (get_name(b), b.get('url')))
681 pop(status='COMPLETED', result='CANCELED',
682 title='Canceled:', color=Fore.MAGENTA,
683 f=lambda b: (get_name(b),))
684 pop(status='COMPLETED', result='FAILURE',
685 failure_reason='INVALID_BUILD_DEFINITION',
686 title='Wrong master/builder name:', color=Fore.MAGENTA,
687 f=lambda b: (get_name(b),))
688 pop(status='COMPLETED', result='FAILURE',
689 title='Other failures:',
690 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
691 pop(status='COMPLETED',
692 title='Other finished:',
693 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
694 pop(status='STARTED',
695 title='Started:', color=Fore.YELLOW,
696 f=lambda b: (get_name(b), b.get('url')))
697 pop(status='SCHEDULED',
698 title='Scheduled:',
699 f=lambda b: (get_name(b), 'id=%s' % b['id']))
700 # The last section is just in case buildbucket API changes OR there is a bug.
701 pop(title='Other:',
702 f=lambda b: (get_name(b), 'id=%s' % b['id']))
703 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700704 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000705
706
qyearsley53f48a12016-09-01 10:45:13 -0700707def write_try_results_json(output_file, builds):
708 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
709
710 The input |builds| dict is assumed to be generated by Buildbucket.
711 Buildbucket documentation: http://goo.gl/G0s101
712 """
713
714 def convert_build_dict(build):
715 return {
716 'buildbucket_id': build.get('id'),
717 'status': build.get('status'),
718 'result': build.get('result'),
719 'bucket': build.get('bucket'),
720 'builder_name': json.loads(
721 build.get('parameters_json', '{}')).get('builder_name'),
722 'failure_reason': build.get('failure_reason'),
723 'url': build.get('url'),
724 }
725
726 converted = []
727 for _, build in sorted(builds.items()):
728 converted.append(convert_build_dict(build))
729 write_json(output_file, converted)
730
731
iannucci@chromium.org79540052012-10-19 23:15:26 +0000732def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000733 """Prints statistics about the change to the user."""
734 # --no-ext-diff is broken in some versions of Git, so try to work around
735 # this by overriding the environment (but there is still a problem if the
736 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000738 if 'GIT_EXTERNAL_DIFF' in env:
739 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000740
741 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800742 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000743 else:
744 similarity_options = ['-M%s' % similarity]
745
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000746 try:
747 stdout = sys.stdout.fileno()
748 except AttributeError:
749 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000750 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000751 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000752 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000753 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000754
755
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000756class BuildbucketResponseException(Exception):
757 pass
758
759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760class Settings(object):
761 def __init__(self):
762 self.default_server = None
763 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000764 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 self.tree_status_url = None
766 self.viewvc_url = None
767 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000768 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000769 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000770 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000771 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000772 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000773 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774
775 def LazyUpdateIfNeeded(self):
776 """Updates the settings from a codereview.settings file, if available."""
777 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000778 # The only value that actually changes the behavior is
779 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000780 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000781 error_ok=True
782 ).strip().lower()
783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000785 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786 LoadCodereviewSettingsFromFile(cr_settings_file)
787 self.updated = True
788
789 def GetDefaultServerUrl(self, error_ok=False):
790 if not self.default_server:
791 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000792 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000793 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 if error_ok:
795 return self.default_server
796 if not self.default_server:
797 error_message = ('Could not find settings file. You must configure '
798 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000799 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000800 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 return self.default_server
802
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000803 @staticmethod
804 def GetRelativeRoot():
805 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000806
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000808 if self.root is None:
809 self.root = os.path.abspath(self.GetRelativeRoot())
810 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000812 def GetGitMirror(self, remote='origin'):
813 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000814 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000815 if not os.path.isdir(local_url):
816 return None
817 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
818 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100819 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100820 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000821 if mirror.exists():
822 return mirror
823 return None
824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825 def GetTreeStatusUrl(self, error_ok=False):
826 if not self.tree_status_url:
827 error_message = ('You must configure your tree status URL by running '
828 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000829 self.tree_status_url = self._GetRietveldConfig(
830 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000831 return self.tree_status_url
832
833 def GetViewVCUrl(self):
834 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000835 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 return self.viewvc_url
837
rmistry@google.com90752582014-01-14 21:04:50 +0000838 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000839 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000840
rmistry@google.com78948ed2015-07-08 23:09:57 +0000841 def GetIsSkipDependencyUpload(self, branch_name):
842 """Returns true if specified branch should skip dep uploads."""
843 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
844 error_ok=True)
845
rmistry@google.com5626a922015-02-26 14:03:30 +0000846 def GetRunPostUploadHook(self):
847 run_post_upload_hook = self._GetRietveldConfig(
848 'run-post-upload-hook', error_ok=True)
849 return run_post_upload_hook == "True"
850
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000851 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000852 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000853
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000854 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000855 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000856
ukai@chromium.orge8077812012-02-03 03:41:46 +0000857 def GetIsGerrit(self):
858 """Return true if this repo is assosiated with gerrit code review system."""
859 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700860 self.is_gerrit = (
861 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000862 return self.is_gerrit
863
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000864 def GetSquashGerritUploads(self):
865 """Return true if uploads to Gerrit should be squashed by default."""
866 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700867 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
868 if self.squash_gerrit_uploads is None:
869 # Default is squash now (http://crbug.com/611892#c23).
870 self.squash_gerrit_uploads = not (
871 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
872 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000873 return self.squash_gerrit_uploads
874
tandriia60502f2016-06-20 02:01:53 -0700875 def GetSquashGerritUploadsOverride(self):
876 """Return True or False if codereview.settings should be overridden.
877
878 Returns None if no override has been defined.
879 """
880 # See also http://crbug.com/611892#c23
881 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
882 error_ok=True).strip()
883 if result == 'true':
884 return True
885 if result == 'false':
886 return False
887 return None
888
tandrii@chromium.org28253532016-04-14 13:46:56 +0000889 def GetGerritSkipEnsureAuthenticated(self):
890 """Return True if EnsureAuthenticated should not be done for Gerrit
891 uploads."""
892 if self.gerrit_skip_ensure_authenticated is None:
893 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000894 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000895 error_ok=True).strip() == 'true')
896 return self.gerrit_skip_ensure_authenticated
897
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000898 def GetGitEditor(self):
899 """Return the editor specified in the git config, or None if none is."""
900 if self.git_editor is None:
901 self.git_editor = self._GetConfig('core.editor', error_ok=True)
902 return self.git_editor or None
903
thestig@chromium.org44202a22014-03-11 19:22:18 +0000904 def GetLintRegex(self):
905 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
906 DEFAULT_LINT_REGEX)
907
908 def GetLintIgnoreRegex(self):
909 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
910 DEFAULT_LINT_IGNORE_REGEX)
911
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000912 def GetProject(self):
913 if not self.project:
914 self.project = self._GetRietveldConfig('project', error_ok=True)
915 return self.project
916
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000917 def _GetRietveldConfig(self, param, **kwargs):
918 return self._GetConfig('rietveld.' + param, **kwargs)
919
rmistry@google.com78948ed2015-07-08 23:09:57 +0000920 def _GetBranchConfig(self, branch_name, param, **kwargs):
921 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
922
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923 def _GetConfig(self, param, **kwargs):
924 self.LazyUpdateIfNeeded()
925 return RunGit(['config', param], **kwargs).strip()
926
927
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100928@contextlib.contextmanager
929def _get_gerrit_project_config_file(remote_url):
930 """Context manager to fetch and store Gerrit's project.config from
931 refs/meta/config branch and store it in temp file.
932
933 Provides a temporary filename or None if there was error.
934 """
935 error, _ = RunGitWithCode([
936 'fetch', remote_url,
937 '+refs/meta/config:refs/git_cl/meta/config'])
938 if error:
939 # Ref doesn't exist or isn't accessible to current user.
940 print('WARNING: failed to fetch project config for %s: %s' %
941 (remote_url, error))
942 yield None
943 return
944
945 error, project_config_data = RunGitWithCode(
946 ['show', 'refs/git_cl/meta/config:project.config'])
947 if error:
948 print('WARNING: project.config file not found')
949 yield None
950 return
951
952 with gclient_utils.temporary_directory() as tempdir:
953 project_config_file = os.path.join(tempdir, 'project.config')
954 gclient_utils.FileWrite(project_config_file, project_config_data)
955 yield project_config_file
956
957
958def _is_git_numberer_enabled(remote_url, remote_ref):
959 """Returns True if Git Numberer is enabled on this ref."""
960 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100961 KNOWN_PROJECTS_WHITELIST = [
962 'chromium/src',
963 'external/webrtc',
964 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100965 'infra/experimental',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100966 ]
967
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100968 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
969 url_parts = urlparse.urlparse(remote_url)
970 project_name = url_parts.path.lstrip('/').rstrip('git./')
971 for known in KNOWN_PROJECTS_WHITELIST:
972 if project_name.endswith(known):
973 break
974 else:
975 # Early exit to avoid extra fetches for repos that aren't using Git
976 # Numberer.
977 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100978
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100979 with _get_gerrit_project_config_file(remote_url) as project_config_file:
980 if project_config_file is None:
981 # Failed to fetch project.config, which shouldn't happen on open source
982 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100983 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100984 def get_opts(x):
985 code, out = RunGitWithCode(
986 ['config', '-f', project_config_file, '--get-all',
987 'plugin.git-numberer.validate-%s-refglob' % x])
988 if code == 0:
989 return out.strip().splitlines()
990 return []
991 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100992
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100993 logging.info('validator config enabled %s disabled %s refglobs for '
994 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000995
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100996 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100997 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100998 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100999 return True
1000 return False
1001
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001002 if match_refglobs(disabled):
1003 return False
1004 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001005
1006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007def ShortBranchName(branch):
1008 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001009 return branch.replace('refs/heads/', '', 1)
1010
1011
1012def GetCurrentBranchRef():
1013 """Returns branch ref (e.g., refs/heads/master) or None."""
1014 return RunGit(['symbolic-ref', 'HEAD'],
1015 stderr=subprocess2.VOID, error_ok=True).strip() or None
1016
1017
1018def GetCurrentBranch():
1019 """Returns current branch or None.
1020
1021 For refs/heads/* branches, returns just last part. For others, full ref.
1022 """
1023 branchref = GetCurrentBranchRef()
1024 if branchref:
1025 return ShortBranchName(branchref)
1026 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027
1028
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001029class _CQState(object):
1030 """Enum for states of CL with respect to Commit Queue."""
1031 NONE = 'none'
1032 DRY_RUN = 'dry_run'
1033 COMMIT = 'commit'
1034
1035 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1036
1037
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001038class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001039 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001040 self.issue = issue
1041 self.patchset = patchset
1042 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001043 assert codereview in (None, 'rietveld', 'gerrit')
1044 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001045
1046 @property
1047 def valid(self):
1048 return self.issue is not None
1049
1050
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001051def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001052 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1053 fail_result = _ParsedIssueNumberArgument()
1054
1055 if arg.isdigit():
1056 return _ParsedIssueNumberArgument(issue=int(arg))
1057 if not arg.startswith('http'):
1058 return fail_result
1059 url = gclient_utils.UpgradeToHttps(arg)
1060 try:
1061 parsed_url = urlparse.urlparse(url)
1062 except ValueError:
1063 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001064
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001065 if codereview is not None:
1066 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1067 return parsed or fail_result
1068
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001069 results = {}
1070 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1071 parsed = cls.ParseIssueURL(parsed_url)
1072 if parsed is not None:
1073 results[name] = parsed
1074
1075 if not results:
1076 return fail_result
1077 if len(results) == 1:
1078 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001079
1080 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1081 # This is likely Gerrit.
1082 return results['gerrit']
1083 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001084 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001085
1086
Aaron Gablea45ee112016-11-22 15:14:38 -08001087class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001088 def __init__(self, issue, url):
1089 self.issue = issue
1090 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001091 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001092
1093 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001094 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001095 self.issue, self.url)
1096
1097
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001098_CommentSummary = collections.namedtuple(
1099 '_CommentSummary', ['date', 'message', 'sender',
1100 # TODO(tandrii): these two aren't known in Gerrit.
1101 'approval', 'disapproval'])
1102
1103
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001105 """Changelist works with one changelist in local branch.
1106
1107 Supports two codereview backends: Rietveld or Gerrit, selected at object
1108 creation.
1109
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001110 Notes:
1111 * Not safe for concurrent multi-{thread,process} use.
1112 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001113 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001114 """
1115
1116 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1117 """Create a new ChangeList instance.
1118
1119 If issue is given, the codereview must be given too.
1120
1121 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1122 Otherwise, it's decided based on current configuration of the local branch,
1123 with default being 'rietveld' for backwards compatibility.
1124 See _load_codereview_impl for more details.
1125
1126 **kwargs will be passed directly to codereview implementation.
1127 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001129 global settings
1130 if not settings:
1131 # Happens when git_cl.py is used as a utility library.
1132 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001133
1134 if issue:
1135 assert codereview, 'codereview must be known, if issue is known'
1136
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 self.branchref = branchref
1138 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001139 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 self.branch = ShortBranchName(self.branchref)
1141 else:
1142 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001144 self.lookedup_issue = False
1145 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146 self.has_description = False
1147 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001148 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001150 self.cc = None
1151 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001152 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001153
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001154 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001155 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001156 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001157 assert self._codereview_impl
1158 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001159
1160 def _load_codereview_impl(self, codereview=None, **kwargs):
1161 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001162 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1163 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1164 self._codereview = codereview
1165 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001166 return
1167
1168 # Automatic selection based on issue number set for a current branch.
1169 # Rietveld takes precedence over Gerrit.
1170 assert not self.issue
1171 # Whether we find issue or not, we are doing the lookup.
1172 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001173 if self.GetBranch():
1174 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1175 issue = _git_get_branch_config_value(
1176 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1177 if issue:
1178 self._codereview = codereview
1179 self._codereview_impl = cls(self, **kwargs)
1180 self.issue = int(issue)
1181 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001182
1183 # No issue is set for this branch, so decide based on repo-wide settings.
1184 return self._load_codereview_impl(
1185 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1186 **kwargs)
1187
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001188 def IsGerrit(self):
1189 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001190
1191 def GetCCList(self):
1192 """Return the users cc'd on this CL.
1193
agable92bec4f2016-08-24 09:27:27 -07001194 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001195 """
1196 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001197 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001198 more_cc = ','.join(self.watchers)
1199 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1200 return self.cc
1201
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001202 def GetCCListWithoutDefault(self):
1203 """Return the users cc'd on this CL excluding default ones."""
1204 if self.cc is None:
1205 self.cc = ','.join(self.watchers)
1206 return self.cc
1207
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001208 def SetWatchers(self, watchers):
1209 """Set the list of email addresses that should be cc'd based on the changed
1210 files in this CL.
1211 """
1212 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213
1214 def GetBranch(self):
1215 """Returns the short branch name, e.g. 'master'."""
1216 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001217 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001218 if not branchref:
1219 return None
1220 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221 self.branch = ShortBranchName(self.branchref)
1222 return self.branch
1223
1224 def GetBranchRef(self):
1225 """Returns the full branch name, e.g. 'refs/heads/master'."""
1226 self.GetBranch() # Poke the lazy loader.
1227 return self.branchref
1228
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001229 def ClearBranch(self):
1230 """Clears cached branch data of this object."""
1231 self.branch = self.branchref = None
1232
tandrii5d48c322016-08-18 16:19:37 -07001233 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1234 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1235 kwargs['branch'] = self.GetBranch()
1236 return _git_get_branch_config_value(key, default, **kwargs)
1237
1238 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1239 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1240 assert self.GetBranch(), (
1241 'this CL must have an associated branch to %sset %s%s' %
1242 ('un' if value is None else '',
1243 key,
1244 '' if value is None else ' to %r' % value))
1245 kwargs['branch'] = self.GetBranch()
1246 return _git_set_branch_config_value(key, value, **kwargs)
1247
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001248 @staticmethod
1249 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001250 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 e.g. 'origin', 'refs/heads/master'
1252 """
1253 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001254 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1255
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001257 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001259 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1260 error_ok=True).strip()
1261 if upstream_branch:
1262 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001264 # Else, try to guess the origin remote.
1265 remote_branches = RunGit(['branch', '-r']).split()
1266 if 'origin/master' in remote_branches:
1267 # Fall back on origin/master if it exits.
1268 remote = 'origin'
1269 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001271 DieWithError(
1272 'Unable to determine default branch to diff against.\n'
1273 'Either pass complete "git diff"-style arguments, like\n'
1274 ' git cl upload origin/master\n'
1275 'or verify this branch is set up to track another \n'
1276 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277
1278 return remote, upstream_branch
1279
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001280 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001281 upstream_branch = self.GetUpstreamBranch()
1282 if not BranchExists(upstream_branch):
1283 DieWithError('The upstream for the current branch (%s) does not exist '
1284 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001285 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001286 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 def GetUpstreamBranch(self):
1289 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001292 upstream_branch = upstream_branch.replace('refs/heads/',
1293 'refs/remotes/%s/' % remote)
1294 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1295 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296 self.upstream_branch = upstream_branch
1297 return self.upstream_branch
1298
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001299 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001300 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 remote, branch = None, self.GetBranch()
1302 seen_branches = set()
1303 while branch not in seen_branches:
1304 seen_branches.add(branch)
1305 remote, branch = self.FetchUpstreamTuple(branch)
1306 branch = ShortBranchName(branch)
1307 if remote != '.' or branch.startswith('refs/remotes'):
1308 break
1309 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001310 remotes = RunGit(['remote'], error_ok=True).split()
1311 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001312 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001313 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001314 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001315 logging.warn('Could not determine which remote this change is '
1316 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001317 else:
1318 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001319 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001320 branch = 'HEAD'
1321 if branch.startswith('refs/remotes'):
1322 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001323 elif branch.startswith('refs/branch-heads/'):
1324 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001325 else:
1326 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001327 return self._remote
1328
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 def GitSanityChecks(self, upstream_git_obj):
1330 """Checks git repo status and ensures diff is from local commits."""
1331
sbc@chromium.org79706062015-01-14 21:18:12 +00001332 if upstream_git_obj is None:
1333 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001334 print('ERROR: unable to determine current branch (detached HEAD?)',
1335 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001336 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001337 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001338 return False
1339
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001340 # Verify the commit we're diffing against is in our current branch.
1341 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1342 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1343 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001344 print('ERROR: %s is not in the current branch. You may need to rebase '
1345 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001346 return False
1347
1348 # List the commits inside the diff, and verify they are all local.
1349 commits_in_diff = RunGit(
1350 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1351 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1352 remote_branch = remote_branch.strip()
1353 if code != 0:
1354 _, remote_branch = self.GetRemoteBranch()
1355
1356 commits_in_remote = RunGit(
1357 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1358
1359 common_commits = set(commits_in_diff) & set(commits_in_remote)
1360 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001361 print('ERROR: Your diff contains %d commits already in %s.\n'
1362 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1363 'the diff. If you are using a custom git flow, you can override'
1364 ' the reference used for this check with "git config '
1365 'gitcl.remotebranch <git-ref>".' % (
1366 len(common_commits), remote_branch, upstream_git_obj),
1367 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001368 return False
1369 return True
1370
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001371 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001372 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001373
1374 Returns None if it is not set.
1375 """
tandrii5d48c322016-08-18 16:19:37 -07001376 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001377
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378 def GetRemoteUrl(self):
1379 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1380
1381 Returns None if there is no remote.
1382 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001383 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001384 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1385
1386 # If URL is pointing to a local directory, it is probably a git cache.
1387 if os.path.isdir(url):
1388 url = RunGit(['config', 'remote.%s.url' % remote],
1389 error_ok=True,
1390 cwd=url).strip()
1391 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001393 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001394 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001395 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001396 self.issue = self._GitGetBranchConfigValue(
1397 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001398 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 return self.issue
1400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 def GetIssueURL(self):
1402 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001403 issue = self.GetIssue()
1404 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001405 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001406 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001408 def GetDescription(self, pretty=False, force=False):
1409 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001411 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 self.has_description = True
1413 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001414 # Set width to 72 columns + 2 space indent.
1415 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001417 lines = self.description.splitlines()
1418 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 return self.description
1420
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001421 def GetDescriptionFooters(self):
1422 """Returns (non_footer_lines, footers) for the commit message.
1423
1424 Returns:
1425 non_footer_lines (list(str)) - Simple list of description lines without
1426 any footer. The lines do not contain newlines, nor does the list contain
1427 the empty line between the message and the footers.
1428 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1429 [("Change-Id", "Ideadbeef...."), ...]
1430 """
1431 raw_description = self.GetDescription()
1432 msg_lines, _, footers = git_footers.split_footers(raw_description)
1433 if footers:
1434 msg_lines = msg_lines[:len(msg_lines)-1]
1435 return msg_lines, footers
1436
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001438 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001439 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001440 self.patchset = self._GitGetBranchConfigValue(
1441 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001442 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 return self.patchset
1444
1445 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001446 """Set this branch's patchset. If patchset=0, clears the patchset."""
1447 assert self.GetBranch()
1448 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001449 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001450 else:
1451 self.patchset = int(patchset)
1452 self._GitSetBranchConfigValue(
1453 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001455 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001456 """Set this branch's issue. If issue isn't given, clears the issue."""
1457 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001459 issue = int(issue)
1460 self._GitSetBranchConfigValue(
1461 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001462 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001463 codereview_server = self._codereview_impl.GetCodereviewServer()
1464 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001465 self._GitSetBranchConfigValue(
1466 self._codereview_impl.CodereviewServerConfigKey(),
1467 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 else:
tandrii5d48c322016-08-18 16:19:37 -07001469 # Reset all of these just to be clean.
1470 reset_suffixes = [
1471 'last-upload-hash',
1472 self._codereview_impl.IssueConfigKey(),
1473 self._codereview_impl.PatchsetConfigKey(),
1474 self._codereview_impl.CodereviewServerConfigKey(),
1475 ] + self._PostUnsetIssueProperties()
1476 for prop in reset_suffixes:
1477 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001478 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001479 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480
dnjba1b0f32016-09-02 12:37:42 -07001481 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001482 if not self.GitSanityChecks(upstream_branch):
1483 DieWithError('\nGit sanity check failure')
1484
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001485 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001486 if not root:
1487 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001488 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001489
1490 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001491 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001492 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001493 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001494 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001495 except subprocess2.CalledProcessError:
1496 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001497 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001498 'This branch probably doesn\'t exist anymore. To reset the\n'
1499 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001500 ' git branch --set-upstream-to origin/master %s\n'
1501 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001502 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001503
maruel@chromium.org52424302012-08-29 15:14:30 +00001504 issue = self.GetIssue()
1505 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001506 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001507 description = self.GetDescription()
1508 else:
1509 # If the change was never uploaded, use the log messages of all commits
1510 # up to the branch point, as git cl upload will prefill the description
1511 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001512 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1513 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001514
1515 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001516 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001517 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001518 name,
1519 description,
1520 absroot,
1521 files,
1522 issue,
1523 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001524 author,
1525 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001526
dsansomee2d6fd92016-09-08 00:10:47 -07001527 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001528 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001529 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001530 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001531
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001532 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1533 """Sets the description for this CL remotely.
1534
1535 You can get description_lines and footers with GetDescriptionFooters.
1536
1537 Args:
1538 description_lines (list(str)) - List of CL description lines without
1539 newline characters.
1540 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1541 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1542 `List-Of-Tokens`). It will be case-normalized so that each token is
1543 title-cased.
1544 """
1545 new_description = '\n'.join(description_lines)
1546 if footers:
1547 new_description += '\n'
1548 for k, v in footers:
1549 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1550 if not git_footers.FOOTER_PATTERN.match(foot):
1551 raise ValueError('Invalid footer %r' % foot)
1552 new_description += foot + '\n'
1553 self.UpdateDescription(new_description, force)
1554
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001555 def RunHook(self, committing, may_prompt, verbose, change):
1556 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1557 try:
1558 return presubmit_support.DoPresubmitChecks(change, committing,
1559 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1560 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001561 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1562 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001563 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001564 DieWithError(
1565 ('%s\nMaybe your depot_tools is out of date?\n'
1566 'If all fails, contact maruel@') % e)
1567
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001568 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1569 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001570 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1571 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001572 else:
1573 # Assume url.
1574 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1575 urlparse.urlparse(issue_arg))
1576 if not parsed_issue_arg or not parsed_issue_arg.valid:
1577 DieWithError('Failed to parse issue argument "%s". '
1578 'Must be an issue number or a valid URL.' % issue_arg)
1579 return self._codereview_impl.CMDPatchWithParsedIssue(
1580 parsed_issue_arg, reject, nocommit, directory)
1581
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001582 def CMDUpload(self, options, git_diff_args, orig_args):
1583 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001584 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001585 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001586 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001587 else:
1588 if self.GetBranch() is None:
1589 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1590
1591 # Default to diffing against common ancestor of upstream branch
1592 base_branch = self.GetCommonAncestorWithUpstream()
1593 git_diff_args = [base_branch, 'HEAD']
1594
Aaron Gablec4c40d12017-05-22 11:49:53 -07001595 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1596 if not self.IsGerrit() and not self.GetIssue():
1597 print('=====================================')
1598 print('NOTICE: Rietveld is being deprecated. '
1599 'You can upload changes to Gerrit with')
1600 print(' git cl upload --gerrit')
1601 print('or set Gerrit to be your default code review tool with')
1602 print(' git config gerrit.host true')
1603 print('=====================================')
1604
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001605 # Fast best-effort checks to abort before running potentially
1606 # expensive hooks if uploading is likely to fail anyway. Passing these
1607 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001608 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001609 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001610
1611 # Apply watchlists on upload.
1612 change = self.GetChange(base_branch, None)
1613 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1614 files = [f.LocalPath() for f in change.AffectedFiles()]
1615 if not options.bypass_watchlists:
1616 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1617
1618 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001619 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001620 # Set the reviewer list now so that presubmit checks can access it.
1621 change_description = ChangeDescription(change.FullDescriptionText())
1622 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001623 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001624 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001625 change)
1626 change.SetDescriptionText(change_description.description)
1627 hook_results = self.RunHook(committing=False,
1628 may_prompt=not options.force,
1629 verbose=options.verbose,
1630 change=change)
1631 if not hook_results.should_continue():
1632 return 1
1633 if not options.reviewers and hook_results.reviewers:
1634 options.reviewers = hook_results.reviewers.split(',')
1635
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001636 # TODO(tandrii): Checking local patchset against remote patchset is only
1637 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1638 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001639 latest_patchset = self.GetMostRecentPatchset()
1640 local_patchset = self.GetPatchset()
1641 if (latest_patchset and local_patchset and
1642 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001643 print('The last upload made from this repository was patchset #%d but '
1644 'the most recent patchset on the server is #%d.'
1645 % (local_patchset, latest_patchset))
1646 print('Uploading will still work, but if you\'ve uploaded to this '
1647 'issue from another machine or branch the patch you\'re '
1648 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001649 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001650
1651 print_stats(options.similarity, options.find_copies, git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001652 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001653 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001654 if options.use_commit_queue:
1655 self.SetCQState(_CQState.COMMIT)
1656 elif options.cq_dry_run:
1657 self.SetCQState(_CQState.DRY_RUN)
1658
tandrii5d48c322016-08-18 16:19:37 -07001659 _git_set_branch_config_value('last-upload-hash',
1660 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001661 # Run post upload hooks, if specified.
1662 if settings.GetRunPostUploadHook():
1663 presubmit_support.DoPostUploadExecuter(
1664 change,
1665 self,
1666 settings.GetRoot(),
1667 options.verbose,
1668 sys.stdout)
1669
1670 # Upload all dependencies if specified.
1671 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001672 print()
1673 print('--dependencies has been specified.')
1674 print('All dependent local branches will be re-uploaded.')
1675 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001676 # Remove the dependencies flag from args so that we do not end up in a
1677 # loop.
1678 orig_args.remove('--dependencies')
1679 ret = upload_branch_deps(self, orig_args)
1680 return ret
1681
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001682 def SetCQState(self, new_state):
1683 """Update the CQ state for latest patchset.
1684
1685 Issue must have been already uploaded and known.
1686 """
1687 assert new_state in _CQState.ALL_STATES
1688 assert self.GetIssue()
1689 return self._codereview_impl.SetCQState(new_state)
1690
qyearsley1fdfcb62016-10-24 13:22:03 -07001691 def TriggerDryRun(self):
1692 """Triggers a dry run and prints a warning on failure."""
1693 # TODO(qyearsley): Either re-use this method in CMDset_commit
1694 # and CMDupload, or change CMDtry to trigger dry runs with
1695 # just SetCQState, and catch keyboard interrupt and other
1696 # errors in that method.
1697 try:
1698 self.SetCQState(_CQState.DRY_RUN)
1699 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1700 return 0
1701 except KeyboardInterrupt:
1702 raise
1703 except:
1704 print('WARNING: failed to trigger CQ Dry Run.\n'
1705 'Either:\n'
1706 ' * your project has no CQ\n'
1707 ' * you don\'t have permission to trigger Dry Run\n'
1708 ' * bug in this code (see stack trace below).\n'
1709 'Consider specifying which bots to trigger manually '
1710 'or asking your project owners for permissions '
1711 'or contacting Chrome Infrastructure team at '
1712 'https://www.chromium.org/infra\n\n')
1713 # Still raise exception so that stack trace is printed.
1714 raise
1715
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001716 # Forward methods to codereview specific implementation.
1717
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001718 def AddComment(self, message):
1719 return self._codereview_impl.AddComment(message)
1720
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001721 def GetCommentsSummary(self):
1722 """Returns list of _CommentSummary for each comment.
1723
1724 Note: comments per file or per line are not included,
1725 only top-level comments are returned.
1726 """
1727 return self._codereview_impl.GetCommentsSummary()
1728
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001729 def CloseIssue(self):
1730 return self._codereview_impl.CloseIssue()
1731
1732 def GetStatus(self):
1733 return self._codereview_impl.GetStatus()
1734
1735 def GetCodereviewServer(self):
1736 return self._codereview_impl.GetCodereviewServer()
1737
tandriide281ae2016-10-12 06:02:30 -07001738 def GetIssueOwner(self):
1739 """Get owner from codereview, which may differ from this checkout."""
1740 return self._codereview_impl.GetIssueOwner()
1741
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001742 def GetMostRecentPatchset(self):
1743 return self._codereview_impl.GetMostRecentPatchset()
1744
tandriide281ae2016-10-12 06:02:30 -07001745 def CannotTriggerTryJobReason(self):
1746 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1747 return self._codereview_impl.CannotTriggerTryJobReason()
1748
tandrii8c5a3532016-11-04 07:52:02 -07001749 def GetTryjobProperties(self, patchset=None):
1750 """Returns dictionary of properties to launch tryjob."""
1751 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1752
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753 def __getattr__(self, attr):
1754 # This is because lots of untested code accesses Rietveld-specific stuff
1755 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001756 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001757 # Note that child method defines __getattr__ as well, and forwards it here,
1758 # because _RietveldChangelistImpl is not cleaned up yet, and given
1759 # deprecation of Rietveld, it should probably be just removed.
1760 # Until that time, avoid infinite recursion by bypassing __getattr__
1761 # of implementation class.
1762 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001763
1764
1765class _ChangelistCodereviewBase(object):
1766 """Abstract base class encapsulating codereview specifics of a changelist."""
1767 def __init__(self, changelist):
1768 self._changelist = changelist # instance of Changelist
1769
1770 def __getattr__(self, attr):
1771 # Forward methods to changelist.
1772 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1773 # _RietveldChangelistImpl to avoid this hack?
1774 return getattr(self._changelist, attr)
1775
1776 def GetStatus(self):
1777 """Apply a rough heuristic to give a simple summary of an issue's review
1778 or CQ status, assuming adherence to a common workflow.
1779
1780 Returns None if no issue for this branch, or specific string keywords.
1781 """
1782 raise NotImplementedError()
1783
1784 def GetCodereviewServer(self):
1785 """Returns server URL without end slash, like "https://codereview.com"."""
1786 raise NotImplementedError()
1787
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001788 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001789 """Fetches and returns description from the codereview server."""
1790 raise NotImplementedError()
1791
tandrii5d48c322016-08-18 16:19:37 -07001792 @classmethod
1793 def IssueConfigKey(cls):
1794 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001795 raise NotImplementedError()
1796
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001797 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001798 def PatchsetConfigKey(cls):
1799 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001800 raise NotImplementedError()
1801
tandrii5d48c322016-08-18 16:19:37 -07001802 @classmethod
1803 def CodereviewServerConfigKey(cls):
1804 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001805 raise NotImplementedError()
1806
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001807 def _PostUnsetIssueProperties(self):
1808 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001809 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001810
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001811 def GetRieveldObjForPresubmit(self):
1812 # This is an unfortunate Rietveld-embeddedness in presubmit.
1813 # For non-Rietveld codereviews, this probably should return a dummy object.
1814 raise NotImplementedError()
1815
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001816 def GetGerritObjForPresubmit(self):
1817 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1818 return None
1819
dsansomee2d6fd92016-09-08 00:10:47 -07001820 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001821 """Update the description on codereview site."""
1822 raise NotImplementedError()
1823
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001824 def AddComment(self, message):
1825 """Posts a comment to the codereview site."""
1826 raise NotImplementedError()
1827
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001828 def GetCommentsSummary(self):
1829 raise NotImplementedError()
1830
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001831 def CloseIssue(self):
1832 """Closes the issue."""
1833 raise NotImplementedError()
1834
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001835 def GetMostRecentPatchset(self):
1836 """Returns the most recent patchset number from the codereview site."""
1837 raise NotImplementedError()
1838
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001839 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1840 directory):
1841 """Fetches and applies the issue.
1842
1843 Arguments:
1844 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1845 reject: if True, reject the failed patch instead of switching to 3-way
1846 merge. Rietveld only.
1847 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1848 only.
1849 directory: switch to directory before applying the patch. Rietveld only.
1850 """
1851 raise NotImplementedError()
1852
1853 @staticmethod
1854 def ParseIssueURL(parsed_url):
1855 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1856 failed."""
1857 raise NotImplementedError()
1858
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001859 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001860 """Best effort check that user is authenticated with codereview server.
1861
1862 Arguments:
1863 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001864 refresh: whether to attempt to refresh credentials. Ignored if not
1865 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001866 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001867 raise NotImplementedError()
1868
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001869 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001870 """Best effort check that uploading isn't supposed to fail for predictable
1871 reasons.
1872
1873 This method should raise informative exception if uploading shouldn't
1874 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001875
1876 Arguments:
1877 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001878 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001879 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001880
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001881 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001882 """Uploads a change to codereview."""
1883 raise NotImplementedError()
1884
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001885 def SetCQState(self, new_state):
1886 """Update the CQ state for latest patchset.
1887
1888 Issue must have been already uploaded and known.
1889 """
1890 raise NotImplementedError()
1891
tandriie113dfd2016-10-11 10:20:12 -07001892 def CannotTriggerTryJobReason(self):
1893 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1894 raise NotImplementedError()
1895
tandriide281ae2016-10-12 06:02:30 -07001896 def GetIssueOwner(self):
1897 raise NotImplementedError()
1898
tandrii8c5a3532016-11-04 07:52:02 -07001899 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001900 raise NotImplementedError()
1901
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001902
1903class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001904 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001905 super(_RietveldChangelistImpl, self).__init__(changelist)
1906 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001907 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001908 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001909
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001910 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001911 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001912 self._props = None
1913 self._rpc_server = None
1914
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001915 def GetCodereviewServer(self):
1916 if not self._rietveld_server:
1917 # If we're on a branch then get the server potentially associated
1918 # with that branch.
1919 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001920 self._rietveld_server = gclient_utils.UpgradeToHttps(
1921 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001922 if not self._rietveld_server:
1923 self._rietveld_server = settings.GetDefaultServerUrl()
1924 return self._rietveld_server
1925
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001926 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001927 """Best effort check that user is authenticated with Rietveld server."""
1928 if self._auth_config.use_oauth2:
1929 authenticator = auth.get_authenticator_for_host(
1930 self.GetCodereviewServer(), self._auth_config)
1931 if not authenticator.has_cached_credentials():
1932 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001933 if refresh:
1934 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001935
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001936 def EnsureCanUploadPatchset(self, force):
1937 # No checks for Rietveld because we are deprecating Rietveld.
1938 pass
1939
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001940 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001941 issue = self.GetIssue()
1942 assert issue
1943 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001944 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001945 except urllib2.HTTPError as e:
1946 if e.code == 404:
1947 DieWithError(
1948 ('\nWhile fetching the description for issue %d, received a '
1949 '404 (not found)\n'
1950 'error. It is likely that you deleted this '
1951 'issue on the server. If this is the\n'
1952 'case, please run\n\n'
1953 ' git cl issue 0\n\n'
1954 'to clear the association with the deleted issue. Then run '
1955 'this command again.') % issue)
1956 else:
1957 DieWithError(
1958 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1959 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001960 print('Warning: Failed to retrieve CL description due to network '
1961 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001962 return ''
1963
1964 def GetMostRecentPatchset(self):
1965 return self.GetIssueProperties()['patchsets'][-1]
1966
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001967 def GetIssueProperties(self):
1968 if self._props is None:
1969 issue = self.GetIssue()
1970 if not issue:
1971 self._props = {}
1972 else:
1973 self._props = self.RpcServer().get_issue_properties(issue, True)
1974 return self._props
1975
tandriie113dfd2016-10-11 10:20:12 -07001976 def CannotTriggerTryJobReason(self):
1977 props = self.GetIssueProperties()
1978 if not props:
1979 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1980 if props.get('closed'):
1981 return 'CL %s is closed' % self.GetIssue()
1982 if props.get('private'):
1983 return 'CL %s is private' % self.GetIssue()
1984 return None
1985
tandrii8c5a3532016-11-04 07:52:02 -07001986 def GetTryjobProperties(self, patchset=None):
1987 """Returns dictionary of properties to launch tryjob."""
1988 project = (self.GetIssueProperties() or {}).get('project')
1989 return {
1990 'issue': self.GetIssue(),
1991 'patch_project': project,
1992 'patch_storage': 'rietveld',
1993 'patchset': patchset or self.GetPatchset(),
1994 'rietveld': self.GetCodereviewServer(),
1995 }
1996
tandriide281ae2016-10-12 06:02:30 -07001997 def GetIssueOwner(self):
1998 return (self.GetIssueProperties() or {}).get('owner_email')
1999
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002000 def AddComment(self, message):
2001 return self.RpcServer().add_comment(self.GetIssue(), message)
2002
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002003 def GetCommentsSummary(self):
2004 summary = []
2005 for message in self.GetIssueProperties().get('messages', []):
2006 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2007 summary.append(_CommentSummary(
2008 date=date,
2009 disapproval=bool(message['disapproval']),
2010 approval=bool(message['approval']),
2011 sender=message['sender'],
2012 message=message['text'],
2013 ))
2014 return summary
2015
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002016 def GetStatus(self):
2017 """Apply a rough heuristic to give a simple summary of an issue's review
2018 or CQ status, assuming adherence to a common workflow.
2019
2020 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002021 * 'error' - error from review tool (including deleted issues)
2022 * 'unsent' - not sent for review
2023 * 'waiting' - waiting for review
2024 * 'reply' - waiting for owner to reply to review
2025 * 'not lgtm' - Code-Review label has been set negatively
2026 * 'lgtm' - LGTM from at least one approved reviewer
2027 * 'commit' - in the commit queue
2028 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002029 """
2030 if not self.GetIssue():
2031 return None
2032
2033 try:
2034 props = self.GetIssueProperties()
2035 except urllib2.HTTPError:
2036 return 'error'
2037
2038 if props.get('closed'):
2039 # Issue is closed.
2040 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002041 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002042 # Issue is in the commit queue.
2043 return 'commit'
2044
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002045 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002046 if not messages:
2047 # No message was sent.
2048 return 'unsent'
2049
2050 if get_approving_reviewers(props):
2051 return 'lgtm'
2052 elif get_approving_reviewers(props, disapproval=True):
2053 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002054
tandrii9d2c7a32016-06-22 03:42:45 -07002055 # Skip CQ messages that don't require owner's action.
2056 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2057 if 'Dry run:' in messages[-1]['text']:
2058 messages.pop()
2059 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2060 # This message always follows prior messages from CQ,
2061 # so skip this too.
2062 messages.pop()
2063 else:
2064 # This is probably a CQ messages warranting user attention.
2065 break
2066
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002067 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002068 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002069 return 'reply'
2070 return 'waiting'
2071
dsansomee2d6fd92016-09-08 00:10:47 -07002072 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002073 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002074
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002075 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002076 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002077
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002078 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002079 return self.SetFlags({flag: value})
2080
2081 def SetFlags(self, flags):
2082 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002083 """
phajdan.jr68598232016-08-10 03:28:28 -07002084 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002085 try:
tandrii4b233bd2016-07-06 03:50:29 -07002086 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002087 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002088 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002089 if e.code == 404:
2090 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2091 if e.code == 403:
2092 DieWithError(
2093 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002094 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002095 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002096
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002097 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098 """Returns an upload.RpcServer() to access this review's rietveld instance.
2099 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002100 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002101 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002102 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002103 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002104 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002105
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002106 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002107 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002108 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002109
tandrii5d48c322016-08-18 16:19:37 -07002110 @classmethod
2111 def PatchsetConfigKey(cls):
2112 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113
tandrii5d48c322016-08-18 16:19:37 -07002114 @classmethod
2115 def CodereviewServerConfigKey(cls):
2116 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002117
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002118 def GetRieveldObjForPresubmit(self):
2119 return self.RpcServer()
2120
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002121 def SetCQState(self, new_state):
2122 props = self.GetIssueProperties()
2123 if props.get('private'):
2124 DieWithError('Cannot set-commit on private issue')
2125
2126 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002127 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002128 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002129 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002130 else:
tandrii4b233bd2016-07-06 03:50:29 -07002131 assert new_state == _CQState.DRY_RUN
2132 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002133
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002134 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2135 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002136 # PatchIssue should never be called with a dirty tree. It is up to the
2137 # caller to check this, but just in case we assert here since the
2138 # consequences of the caller not checking this could be dire.
2139 assert(not git_common.is_dirty_git_tree('apply'))
2140 assert(parsed_issue_arg.valid)
2141 self._changelist.issue = parsed_issue_arg.issue
2142 if parsed_issue_arg.hostname:
2143 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2144
skobes6468b902016-10-24 08:45:10 -07002145 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2146 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2147 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002148 try:
skobes6468b902016-10-24 08:45:10 -07002149 scm_obj.apply_patch(patchset_object)
2150 except Exception as e:
2151 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002152 return 1
2153
2154 # If we had an issue, commit the current state and register the issue.
2155 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002156 self.SetIssue(self.GetIssue())
2157 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002158 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2159 'patch from issue %(i)s at patchset '
2160 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2161 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002162 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002163 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002164 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002165 return 0
2166
2167 @staticmethod
2168 def ParseIssueURL(parsed_url):
2169 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2170 return None
wychen3c1c1722016-08-04 11:46:36 -07002171 # Rietveld patch: https://domain/<number>/#ps<patchset>
2172 match = re.match(r'/(\d+)/$', parsed_url.path)
2173 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2174 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002175 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002176 issue=int(match.group(1)),
2177 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002178 hostname=parsed_url.netloc,
2179 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002180 # Typical url: https://domain/<issue_number>[/[other]]
2181 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2182 if match:
skobes6468b902016-10-24 08:45:10 -07002183 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002184 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002185 hostname=parsed_url.netloc,
2186 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002187 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2188 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2189 if match:
skobes6468b902016-10-24 08:45:10 -07002190 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002191 issue=int(match.group(1)),
2192 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002193 hostname=parsed_url.netloc,
2194 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002195 return None
2196
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002197 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002198 """Upload the patch to Rietveld."""
2199 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2200 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002201 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2202 if options.emulate_svn_auto_props:
2203 upload_args.append('--emulate_svn_auto_props')
2204
2205 change_desc = None
2206
2207 if options.email is not None:
2208 upload_args.extend(['--email', options.email])
2209
2210 if self.GetIssue():
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])
2213 if options.message:
2214 upload_args.extend(['--message', options.message])
2215 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002216 print('This branch is associated with issue %s. '
2217 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002218 else:
nodirca166002016-06-27 10:59:51 -07002219 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002221 if options.message:
2222 message = options.message
2223 else:
2224 message = CreateDescriptionFromLog(args)
2225 if options.title:
2226 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002227 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002228 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002229 change_desc.update_reviewers(options.reviewers, options.tbrs,
2230 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002231 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002232 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002233
2234 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002235 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002236 return 1
2237
2238 upload_args.extend(['--message', change_desc.description])
2239 if change_desc.get_reviewers():
2240 upload_args.append('--reviewers=%s' % ','.join(
2241 change_desc.get_reviewers()))
2242 if options.send_mail:
2243 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002244 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002245 upload_args.append('--send_mail')
2246
2247 # We check this before applying rietveld.private assuming that in
2248 # rietveld.cc only addresses which we can send private CLs to are listed
2249 # if rietveld.private is set, and so we should ignore rietveld.cc only
2250 # when --private is specified explicitly on the command line.
2251 if options.private:
2252 logging.warn('rietveld.cc is ignored since private flag is specified. '
2253 'You need to review and add them manually if necessary.')
2254 cc = self.GetCCListWithoutDefault()
2255 else:
2256 cc = self.GetCCList()
2257 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002258 if change_desc.get_cced():
2259 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002260 if cc:
2261 upload_args.extend(['--cc', cc])
2262
2263 if options.private or settings.GetDefaultPrivateFlag() == "True":
2264 upload_args.append('--private')
2265
2266 upload_args.extend(['--git_similarity', str(options.similarity)])
2267 if not options.find_copies:
2268 upload_args.extend(['--git_no_find_copies'])
2269
2270 # Include the upstream repo's URL in the change -- this is useful for
2271 # projects that have their source spread across multiple repos.
2272 remote_url = self.GetGitBaseUrlFromConfig()
2273 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002274 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2275 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2276 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002277 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002278 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002279 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002280 if target_ref:
2281 upload_args.extend(['--target_ref', target_ref])
2282
2283 # Look for dependent patchsets. See crbug.com/480453 for more details.
2284 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2285 upstream_branch = ShortBranchName(upstream_branch)
2286 if remote is '.':
2287 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002288 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002289 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002290 print()
2291 print('Skipping dependency patchset upload because git config '
2292 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2293 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002294 else:
2295 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002296 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002297 auth_config=auth_config)
2298 branch_cl_issue_url = branch_cl.GetIssueURL()
2299 branch_cl_issue = branch_cl.GetIssue()
2300 branch_cl_patchset = branch_cl.GetPatchset()
2301 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2302 upload_args.extend(
2303 ['--depends_on_patchset', '%s:%s' % (
2304 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002305 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002306 '\n'
2307 'The current branch (%s) is tracking a local branch (%s) with '
2308 'an associated CL.\n'
2309 'Adding %s/#ps%s as a dependency patchset.\n'
2310 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2311 branch_cl_patchset))
2312
2313 project = settings.GetProject()
2314 if project:
2315 upload_args.extend(['--project', project])
2316
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002317 try:
2318 upload_args = ['upload'] + upload_args + args
2319 logging.info('upload.RealMain(%s)', upload_args)
2320 issue, patchset = upload.RealMain(upload_args)
2321 issue = int(issue)
2322 patchset = int(patchset)
2323 except KeyboardInterrupt:
2324 sys.exit(1)
2325 except:
2326 # If we got an exception after the user typed a description for their
2327 # change, back up the description before re-raising.
2328 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002329 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002330 raise
2331
2332 if not self.GetIssue():
2333 self.SetIssue(issue)
2334 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002335 return 0
2336
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002337
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002338class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002339 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002340 # auth_config is Rietveld thing, kept here to preserve interface only.
2341 super(_GerritChangelistImpl, self).__init__(changelist)
2342 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002343 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002344 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002345 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002346 # Map from change number (issue) to its detail cache.
2347 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002348
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002349 if codereview_host is not None:
2350 assert not codereview_host.startswith('https://'), codereview_host
2351 self._gerrit_host = codereview_host
2352 self._gerrit_server = 'https://%s' % codereview_host
2353
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002354 def _GetGerritHost(self):
2355 # Lazy load of configs.
2356 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002357 if self._gerrit_host and '.' not in self._gerrit_host:
2358 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2359 # This happens for internal stuff http://crbug.com/614312.
2360 parsed = urlparse.urlparse(self.GetRemoteUrl())
2361 if parsed.scheme == 'sso':
2362 print('WARNING: using non https URLs for remote is likely broken\n'
2363 ' Your current remote is: %s' % self.GetRemoteUrl())
2364 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2365 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002366 return self._gerrit_host
2367
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002368 def _GetGitHost(self):
2369 """Returns git host to be used when uploading change to Gerrit."""
2370 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2371
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002372 def GetCodereviewServer(self):
2373 if not self._gerrit_server:
2374 # If we're on a branch then get the server potentially associated
2375 # with that branch.
2376 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002377 self._gerrit_server = self._GitGetBranchConfigValue(
2378 self.CodereviewServerConfigKey())
2379 if self._gerrit_server:
2380 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002381 if not self._gerrit_server:
2382 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2383 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002384 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002385 parts[0] = parts[0] + '-review'
2386 self._gerrit_host = '.'.join(parts)
2387 self._gerrit_server = 'https://%s' % self._gerrit_host
2388 return self._gerrit_server
2389
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002390 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002391 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002392 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002393
tandrii5d48c322016-08-18 16:19:37 -07002394 @classmethod
2395 def PatchsetConfigKey(cls):
2396 return 'gerritpatchset'
2397
2398 @classmethod
2399 def CodereviewServerConfigKey(cls):
2400 return 'gerritserver'
2401
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002402 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002403 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002404 if settings.GetGerritSkipEnsureAuthenticated():
2405 # For projects with unusual authentication schemes.
2406 # See http://crbug.com/603378.
2407 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002408 # Lazy-loader to identify Gerrit and Git hosts.
2409 if gerrit_util.GceAuthenticator.is_gce():
2410 return
2411 self.GetCodereviewServer()
2412 git_host = self._GetGitHost()
2413 assert self._gerrit_server and self._gerrit_host
2414 cookie_auth = gerrit_util.CookiesAuthenticator()
2415
2416 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2417 git_auth = cookie_auth.get_auth_header(git_host)
2418 if gerrit_auth and git_auth:
2419 if gerrit_auth == git_auth:
2420 return
2421 print((
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002422 'WARNING: you have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002423 ' %s\n'
2424 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002425 ' Consider running the following command:\n'
2426 ' git cl creds-check\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002427 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002428 (git_host, self._gerrit_host,
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002429 cookie_auth.get_new_password_message(git_host)))
2430 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002431 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002432 return
2433 else:
2434 missing = (
2435 [] if gerrit_auth else [self._gerrit_host] +
2436 [] if git_auth else [git_host])
2437 DieWithError('Credentials for the following hosts are required:\n'
2438 ' %s\n'
2439 'These are read from %s (or legacy %s)\n'
2440 '%s' % (
2441 '\n '.join(missing),
2442 cookie_auth.get_gitcookies_path(),
2443 cookie_auth.get_netrc_path(),
2444 cookie_auth.get_new_password_message(git_host)))
2445
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002446 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002447 if not self.GetIssue():
2448 return
2449
2450 # Warm change details cache now to avoid RPCs later, reducing latency for
2451 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002452 self._GetChangeDetail(
2453 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002454
2455 status = self._GetChangeDetail()['status']
2456 if status in ('MERGED', 'ABANDONED'):
2457 DieWithError('Change %s has been %s, new uploads are not allowed' %
2458 (self.GetIssueURL(),
2459 'submitted' if status == 'MERGED' else 'abandoned'))
2460
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002461 if gerrit_util.GceAuthenticator.is_gce():
2462 return
2463 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2464 self._GetGerritHost())
2465 if self.GetIssueOwner() == cookies_user:
2466 return
2467 logging.debug('change %s owner is %s, cookies user is %s',
2468 self.GetIssue(), self.GetIssueOwner(), cookies_user)
2469 # Maybe user has linked accounts or smth like that,
2470 # so ask what Gerrit thinks of this user.
2471 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2472 if details['email'] == self.GetIssueOwner():
2473 return
2474 if not force:
2475 print('WARNING: change %s is owned by %s, but you authenticate to Gerrit '
2476 'as %s.\n'
2477 'Uploading may fail due to lack of permissions.' %
2478 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2479 confirm_or_exit(action='upload')
2480
2481
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002482 def _PostUnsetIssueProperties(self):
2483 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002484 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002485
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002486 def GetRieveldObjForPresubmit(self):
2487 class ThisIsNotRietveldIssue(object):
2488 def __nonzero__(self):
2489 # This is a hack to make presubmit_support think that rietveld is not
2490 # defined, yet still ensure that calls directly result in a decent
2491 # exception message below.
2492 return False
2493
2494 def __getattr__(self, attr):
2495 print(
2496 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2497 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2498 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2499 'or use Rietveld for codereview.\n'
2500 'See also http://crbug.com/579160.' % attr)
2501 raise NotImplementedError()
2502 return ThisIsNotRietveldIssue()
2503
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002504 def GetGerritObjForPresubmit(self):
2505 return presubmit_support.GerritAccessor(self._GetGerritHost())
2506
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002507 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002508 """Apply a rough heuristic to give a simple summary of an issue's review
2509 or CQ status, assuming adherence to a common workflow.
2510
2511 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002512 * 'error' - error from review tool (including deleted issues)
2513 * 'unsent' - no reviewers added
2514 * 'waiting' - waiting for review
2515 * 'reply' - waiting for uploader to reply to review
2516 * 'lgtm' - Code-Review label has been set
2517 * 'commit' - in the commit queue
2518 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002519 """
2520 if not self.GetIssue():
2521 return None
2522
2523 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002524 data = self._GetChangeDetail([
2525 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002526 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002527 return 'error'
2528
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002529 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002530 return 'closed'
2531
Aaron Gable9ab38c62017-04-06 14:36:33 -07002532 if data['labels'].get('Commit-Queue', {}).get('approved'):
2533 # The section will have an "approved" subsection if anyone has voted
2534 # the maximum value on the label.
2535 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002536
Aaron Gable9ab38c62017-04-06 14:36:33 -07002537 if data['labels'].get('Code-Review', {}).get('approved'):
2538 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002539
2540 if not data.get('reviewers', {}).get('REVIEWER', []):
2541 return 'unsent'
2542
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002543 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002544 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2545 last_message_author = messages.pop().get('author', {})
2546 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002547 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2548 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002549 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002550 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002551 if last_message_author.get('_account_id') == owner:
2552 # Most recent message was by owner.
2553 return 'waiting'
2554 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002555 # Some reply from non-owner.
2556 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002557
2558 # Somehow there are no messages even though there are reviewers.
2559 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002560
2561 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002562 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002563 return data['revisions'][data['current_revision']]['_number']
2564
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002565 def FetchDescription(self, force=False):
2566 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2567 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002568 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002569 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002570
dsansomee2d6fd92016-09-08 00:10:47 -07002571 def UpdateDescriptionRemote(self, description, force=False):
2572 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2573 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002574 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002575 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002576 'unpublished edit. Either publish the edit in the Gerrit web UI '
2577 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002578
2579 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2580 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002581 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002582 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002583
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002584 def AddComment(self, message):
2585 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2586 msg=message)
2587
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002588 def GetCommentsSummary(self):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002589 # DETAILED_ACCOUNTS is to get emails in accounts.
2590 data = self._GetChangeDetail(options=['MESSAGES', 'DETAILED_ACCOUNTS'])
2591 summary = []
2592 for msg in data.get('messages', []):
2593 # Gerrit spits out nanoseconds.
2594 assert len(msg['date'].split('.')[-1]) == 9
2595 date = datetime.datetime.strptime(msg['date'][:-3],
2596 '%Y-%m-%d %H:%M:%S.%f')
2597 summary.append(_CommentSummary(
2598 date=date,
2599 message=msg['message'],
2600 sender=msg['author']['email'],
2601 # These could be inferred from the text messages and correlated with
2602 # Code-Review label maximum, however this is not reliable.
2603 # Leaving as is until the need arises.
2604 approval=False,
2605 disapproval=False,
2606 ))
2607 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002608
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002609 def CloseIssue(self):
2610 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2611
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002612 def SubmitIssue(self, wait_for_merge=True):
2613 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2614 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002615
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002616 def _GetChangeDetail(self, options=None, issue=None,
2617 no_cache=False):
2618 """Returns details of the issue by querying Gerrit and caching results.
2619
2620 If fresh data is needed, set no_cache=True which will clear cache and
2621 thus new data will be fetched from Gerrit.
2622 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002623 options = options or []
2624 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002625 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002626
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002627 # Optimization to avoid multiple RPCs:
2628 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2629 'CURRENT_COMMIT' not in options):
2630 options.append('CURRENT_COMMIT')
2631
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002632 # Normalize issue and options for consistent keys in cache.
2633 issue = str(issue)
2634 options = [o.upper() for o in options]
2635
2636 # Check in cache first unless no_cache is True.
2637 if no_cache:
2638 self._detail_cache.pop(issue, None)
2639 else:
2640 options_set = frozenset(options)
2641 for cached_options_set, data in self._detail_cache.get(issue, []):
2642 # Assumption: data fetched before with extra options is suitable
2643 # for return for a smaller set of options.
2644 # For example, if we cached data for
2645 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2646 # and request is for options=[CURRENT_REVISION],
2647 # THEN we can return prior cached data.
2648 if options_set.issubset(cached_options_set):
2649 return data
2650
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002651 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002652 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002653 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002654 except gerrit_util.GerritError as e:
2655 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002656 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002657 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002658
2659 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002660 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002661
agable32978d92016-11-01 12:55:02 -07002662 def _GetChangeCommit(self, issue=None):
2663 issue = issue or self.GetIssue()
2664 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002665 try:
2666 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2667 except gerrit_util.GerritError as e:
2668 if e.http_status == 404:
2669 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2670 raise
agable32978d92016-11-01 12:55:02 -07002671 return data
2672
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002673 def CMDLand(self, force, bypass_hooks, verbose):
2674 if git_common.is_dirty_git_tree('land'):
2675 return 1
tandriid60367b2016-06-22 05:25:12 -07002676 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2677 if u'Commit-Queue' in detail.get('labels', {}):
2678 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002679 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2680 'which can test and land changes for you. '
2681 'Are you sure you wish to bypass it?\n',
2682 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002683
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002684 differs = True
tandriic4344b52016-08-29 06:04:54 -07002685 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002686 # Note: git diff outputs nothing if there is no diff.
2687 if not last_upload or RunGit(['diff', last_upload]).strip():
2688 print('WARNING: some changes from local branch haven\'t been uploaded')
2689 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002690 if detail['current_revision'] == last_upload:
2691 differs = False
2692 else:
2693 print('WARNING: local branch contents differ from latest uploaded '
2694 'patchset')
2695 if differs:
2696 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002697 confirm_or_exit(
2698 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2699 action='submit')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002700 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2701 elif not bypass_hooks:
2702 hook_results = self.RunHook(
2703 committing=True,
2704 may_prompt=not force,
2705 verbose=verbose,
2706 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2707 if not hook_results.should_continue():
2708 return 1
2709
2710 self.SubmitIssue(wait_for_merge=True)
2711 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002712 links = self._GetChangeCommit().get('web_links', [])
2713 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002714 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002715 print('Landed as %s' % link.get('url'))
2716 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002717 return 0
2718
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002719 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2720 directory):
2721 assert not reject
2722 assert not nocommit
2723 assert not directory
2724 assert parsed_issue_arg.valid
2725
2726 self._changelist.issue = parsed_issue_arg.issue
2727
2728 if parsed_issue_arg.hostname:
2729 self._gerrit_host = parsed_issue_arg.hostname
2730 self._gerrit_server = 'https://%s' % self._gerrit_host
2731
tandriic2405f52016-10-10 08:13:15 -07002732 try:
2733 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002734 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002735 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002736
2737 if not parsed_issue_arg.patchset:
2738 # Use current revision by default.
2739 revision_info = detail['revisions'][detail['current_revision']]
2740 patchset = int(revision_info['_number'])
2741 else:
2742 patchset = parsed_issue_arg.patchset
2743 for revision_info in detail['revisions'].itervalues():
2744 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2745 break
2746 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002747 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002748 (parsed_issue_arg.patchset, self.GetIssue()))
2749
2750 fetch_info = revision_info['fetch']['http']
2751 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002752 self.SetIssue(self.GetIssue())
2753 self.SetPatchset(patchset)
Aaron Gabled343c632017-03-15 11:02:26 -07002754 RunGit(['cherry-pick', 'FETCH_HEAD'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002755 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002756 (self.GetIssue(), self.GetPatchset()))
2757 return 0
2758
2759 @staticmethod
2760 def ParseIssueURL(parsed_url):
2761 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2762 return None
2763 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2764 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2765 # Short urls like https://domain/<issue_number> can be used, but don't allow
2766 # specifying the patchset (you'd 404), but we allow that here.
2767 if parsed_url.path == '/':
2768 part = parsed_url.fragment
2769 else:
2770 part = parsed_url.path
2771 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2772 if match:
2773 return _ParsedIssueNumberArgument(
2774 issue=int(match.group(2)),
2775 patchset=int(match.group(4)) if match.group(4) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002776 hostname=parsed_url.netloc,
2777 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002778 return None
2779
tandrii16e0b4e2016-06-07 10:34:28 -07002780 def _GerritCommitMsgHookCheck(self, offer_removal):
2781 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2782 if not os.path.exists(hook):
2783 return
2784 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2785 # custom developer made one.
2786 data = gclient_utils.FileRead(hook)
2787 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2788 return
2789 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002790 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002791 'and may interfere with it in subtle ways.\n'
2792 'We recommend you remove the commit-msg hook.')
2793 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002794 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002795 gclient_utils.rm_file_or_tree(hook)
2796 print('Gerrit commit-msg hook removed.')
2797 else:
2798 print('OK, will keep Gerrit commit-msg hook in place.')
2799
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002800 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002801 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002802 if options.squash and options.no_squash:
2803 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002804
2805 if not options.squash and not options.no_squash:
2806 # Load default for user, repo, squash=true, in this order.
2807 options.squash = settings.GetSquashGerritUploads()
2808 elif options.no_squash:
2809 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002810
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002811 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002812 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002813
Aaron Gableb56ad332017-01-06 15:24:31 -08002814 # This may be None; default fallback value is determined in logic below.
2815 title = options.title
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002816 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002817
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002818 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002819 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002820 if self.GetIssue():
2821 # Try to get the message from a previous upload.
2822 message = self.GetDescription()
2823 if not message:
2824 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002825 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002826 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002827 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002828 if options.message:
2829 # For compatibility with Rietveld, if -m|--message is given on
2830 # command line, title should be the first line of that message,
2831 # which shouldn't be confused with CL description.
2832 default_title = options.message.strip().split()[0]
2833 else:
2834 default_title = RunGit(
2835 ['show', '-s', '--format=%s', 'HEAD']).strip()
Andrii Shyshkalove00a29b2017-04-10 14:48:28 +02002836 if options.force:
2837 title = default_title
2838 else:
2839 title = ask_for_data(
2840 'Title for patchset [%s]: ' % default_title) or default_title
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002841 if title == default_title:
2842 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002843 change_id = self._GetChangeDetail()['change_id']
2844 while True:
2845 footer_change_ids = git_footers.get_footer_change_id(message)
2846 if footer_change_ids == [change_id]:
2847 break
2848 if not footer_change_ids:
2849 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002850 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002851 continue
2852 # There is already a valid footer but with different or several ids.
2853 # Doing this automatically is non-trivial as we don't want to lose
2854 # existing other footers, yet we want to append just 1 desired
2855 # Change-Id. Thus, just create a new footer, but let user verify the
2856 # new description.
2857 message = '%s\n\nChange-Id: %s' % (message, change_id)
2858 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002859 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002860 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002861 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002862 'Please, check the proposed correction to the description, '
2863 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2864 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2865 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002866 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002867 if not options.force:
2868 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002869 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002870 message = change_desc.description
2871 if not message:
2872 DieWithError("Description is empty. Aborting...")
2873 # Continue the while loop.
2874 # Sanity check of this code - we should end up with proper message
2875 # footer.
2876 assert [change_id] == git_footers.get_footer_change_id(message)
2877 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002878 else: # if not self.GetIssue()
2879 if options.message:
2880 message = options.message
2881 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002882 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002883 if options.title:
2884 message = options.title + '\n\n' + message
2885 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002886
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002887 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002888 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002889 # On first upload, patchset title is always this string, while
2890 # --title flag gets converted to first line of message.
2891 title = 'Initial upload'
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002892 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002893 if not change_desc.description:
2894 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002895 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002896 if len(change_ids) > 1:
2897 DieWithError('too many Change-Id footers, at most 1 allowed.')
2898 if not change_ids:
2899 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002900 change_desc.set_description(git_footers.add_footer_change_id(
2901 change_desc.description,
2902 GenerateGerritChangeId(change_desc.description)))
2903 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002904 assert len(change_ids) == 1
2905 change_id = change_ids[0]
2906
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002907 if options.reviewers or options.tbrs or options.add_owners_to:
2908 change_desc.update_reviewers(options.reviewers, options.tbrs,
2909 options.add_owners_to, change)
2910
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002911 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002912 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2913 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002914 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2915 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002916 '-m', change_desc.description]).strip()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002917 else:
2918 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002919 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002920 if not change_desc.description:
2921 DieWithError("Description is empty. Aborting...")
2922
2923 if not git_footers.get_footer_change_id(change_desc.description):
2924 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002925 change_desc.set_description(
2926 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002927 if options.reviewers or options.tbrs or options.add_owners_to:
2928 change_desc.update_reviewers(options.reviewers, options.tbrs,
2929 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002930 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002931 # For no-squash mode, we assume the remote called "origin" is the one we
2932 # want. It is not worthwhile to support different workflows for
2933 # no-squash mode.
2934 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002935 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2936
2937 assert change_desc
2938 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2939 ref_to_push)]).splitlines()
2940 if len(commits) > 1:
2941 print('WARNING: This will upload %d commits. Run the following command '
2942 'to see which commits will be uploaded: ' % len(commits))
2943 print('git log %s..%s' % (parent, ref_to_push))
2944 print('You can also use `git squash-branch` to squash these into a '
2945 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002946 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002947
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002948 if options.reviewers or options.tbrs or options.add_owners_to:
2949 change_desc.update_reviewers(options.reviewers, options.tbrs,
2950 options.add_owners_to, change)
2951
2952 if options.send_mail:
2953 if not change_desc.get_reviewers():
2954 DieWithError('Must specify reviewers to send email.', change_desc)
2955
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002956 # Extra options that can be specified at push time. Doc:
2957 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002958 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002959 if change_desc.get_reviewers(tbr_only=True):
2960 print('Adding self-LGTM (Code-Review +1) because of TBRs')
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002961 refspec_opts.append('l=Code-Review+1')
tandrii99a72f22016-08-17 14:33:24 -07002962
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002963
2964 # TODO(tandrii): options.message should be posted as a comment
2965 # if --send-email is set on non-initial upload as Rietveld used to do it.
2966
Aaron Gable9b713dd2016-12-14 16:04:21 -08002967 if title:
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002968 if not re.match(r'^[\w ]+$', title):
2969 title = re.sub(r'[^\w ]', '', title)
2970 if not automatic_title:
2971 print('WARNING: Patchset title may only contain alphanumeric chars '
2972 'and spaces. You can edit it in the UI. '
2973 'See https://crbug.com/663787.\n'
2974 'Cleaned up title: %s' % title)
2975 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2976 # reverse on its side.
2977 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002978
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002979 # Never notify now because no one is on the review. Notify when we add
2980 # reviewers and CCs below.
2981 refspec_opts.append('notify=NONE')
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002982
agablec6787972016-09-09 16:13:34 -07002983 if options.private:
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002984 refspec_opts.append('draft')
agablec6787972016-09-09 16:13:34 -07002985
rmistry9eadede2016-09-19 11:22:43 -07002986 if options.topic:
2987 # Documentation on Gerrit topics is here:
2988 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002989 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002990
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002991 refspec_suffix = ''
2992 if refspec_opts:
2993 refspec_suffix = '%' + ','.join(refspec_opts)
2994 assert ' ' not in refspec_suffix, (
2995 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2996 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2997
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002998 try:
2999 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003000 ['git', 'push', self.GetRemoteUrl(), refspec],
3001 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003002 # Flush after every line: useful for seeing progress when running as
3003 # recipe.
3004 filter_fn=lambda _: sys.stdout.flush())
3005 except subprocess2.CalledProcessError:
3006 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003007 'for the reason of the failure.\n'
3008 'Hint: run command below to diangose common Git/Gerrit '
3009 'credential problems:\n'
3010 ' git cl creds-check\n',
3011 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003012
3013 if options.squash:
3014 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
3015 change_numbers = [m.group(1)
3016 for m in map(regex.match, push_stdout.splitlines())
3017 if m]
3018 if len(change_numbers) != 1:
3019 DieWithError(
3020 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003021 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003022 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003023 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003024
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003025 reviewers = sorted(change_desc.get_reviewers())
3026
tandrii88189772016-09-29 04:29:57 -07003027 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003028 if not options.private:
3029 cc = self.GetCCList().split(',')
3030 else:
3031 cc = []
tandrii88189772016-09-29 04:29:57 -07003032 if options.cc:
3033 cc.extend(options.cc)
3034 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003035 if change_desc.get_cced():
3036 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003037
3038 gerrit_util.AddReviewers(
3039 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3040 notify=bool(options.send_mail))
3041
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003042 return 0
3043
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003044 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3045 change_desc):
3046 """Computes parent of the generated commit to be uploaded to Gerrit.
3047
3048 Returns revision or a ref name.
3049 """
3050 if custom_cl_base:
3051 # Try to avoid creating additional unintended CLs when uploading, unless
3052 # user wants to take this risk.
3053 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3054 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3055 local_ref_of_target_remote])
3056 if code == 1:
3057 print('\nWARNING: manually specified base of this CL `%s` '
3058 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3059 'If you proceed with upload, more than 1 CL may be created by '
3060 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3061 'If you are certain that specified base `%s` has already been '
3062 'uploaded to Gerrit as another CL, you may proceed.\n' %
3063 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3064 if not force:
3065 confirm_or_exit(
3066 'Do you take responsibility for cleaning up potential mess '
3067 'resulting from proceeding with upload?',
3068 action='upload')
3069 return custom_cl_base
3070
Aaron Gablef97e33d2017-03-30 15:44:27 -07003071 if remote != '.':
3072 return self.GetCommonAncestorWithUpstream()
3073
3074 # If our upstream branch is local, we base our squashed commit on its
3075 # squashed version.
3076 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3077
Aaron Gablef97e33d2017-03-30 15:44:27 -07003078 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003079 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003080
3081 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003082 # TODO(tandrii): consider checking parent change in Gerrit and using its
3083 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3084 # the tree hash of the parent branch. The upside is less likely bogus
3085 # requests to reupload parent change just because it's uploadhash is
3086 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003087 parent = RunGit(['config',
3088 'branch.%s.gerritsquashhash' % upstream_branch_name],
3089 error_ok=True).strip()
3090 # Verify that the upstream branch has been uploaded too, otherwise
3091 # Gerrit will create additional CLs when uploading.
3092 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3093 RunGitSilent(['rev-parse', parent + ':'])):
3094 DieWithError(
3095 '\nUpload upstream branch %s first.\n'
3096 'It is likely that this branch has been rebased since its last '
3097 'upload, so you just need to upload it again.\n'
3098 '(If you uploaded it with --no-squash, then branch dependencies '
3099 'are not supported, and you should reupload with --squash.)'
3100 % upstream_branch_name,
3101 change_desc)
3102 return parent
3103
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003104 def _AddChangeIdToCommitMessage(self, options, args):
3105 """Re-commits using the current message, assumes the commit hook is in
3106 place.
3107 """
3108 log_desc = options.message or CreateDescriptionFromLog(args)
3109 git_command = ['commit', '--amend', '-m', log_desc]
3110 RunGit(git_command)
3111 new_log_desc = CreateDescriptionFromLog(args)
3112 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003113 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003114 return new_log_desc
3115 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003116 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003117
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003118 def SetCQState(self, new_state):
3119 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003120 vote_map = {
3121 _CQState.NONE: 0,
3122 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003123 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003124 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01003125 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
3126 if new_state == _CQState.DRY_RUN:
3127 # Don't spam everybody reviewer/owner.
3128 kwargs['notify'] = 'NONE'
3129 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003130
tandriie113dfd2016-10-11 10:20:12 -07003131 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003132 try:
3133 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003134 except GerritChangeNotExists:
3135 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003136
3137 if data['status'] in ('ABANDONED', 'MERGED'):
3138 return 'CL %s is closed' % self.GetIssue()
3139
3140 def GetTryjobProperties(self, patchset=None):
3141 """Returns dictionary of properties to launch tryjob."""
3142 data = self._GetChangeDetail(['ALL_REVISIONS'])
3143 patchset = int(patchset or self.GetPatchset())
3144 assert patchset
3145 revision_data = None # Pylint wants it to be defined.
3146 for revision_data in data['revisions'].itervalues():
3147 if int(revision_data['_number']) == patchset:
3148 break
3149 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003150 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003151 (patchset, self.GetIssue()))
3152 return {
3153 'patch_issue': self.GetIssue(),
3154 'patch_set': patchset or self.GetPatchset(),
3155 'patch_project': data['project'],
3156 'patch_storage': 'gerrit',
3157 'patch_ref': revision_data['fetch']['http']['ref'],
3158 'patch_repository_url': revision_data['fetch']['http']['url'],
3159 'patch_gerrit_url': self.GetCodereviewServer(),
3160 }
tandriie113dfd2016-10-11 10:20:12 -07003161
tandriide281ae2016-10-12 06:02:30 -07003162 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003163 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003164
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003165
3166_CODEREVIEW_IMPLEMENTATIONS = {
3167 'rietveld': _RietveldChangelistImpl,
3168 'gerrit': _GerritChangelistImpl,
3169}
3170
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003171
iannuccie53c9352016-08-17 14:40:40 -07003172def _add_codereview_issue_select_options(parser, extra=""):
3173 _add_codereview_select_options(parser)
3174
3175 text = ('Operate on this issue number instead of the current branch\'s '
3176 'implicit issue.')
3177 if extra:
3178 text += ' '+extra
3179 parser.add_option('-i', '--issue', type=int, help=text)
3180
3181
3182def _process_codereview_issue_select_options(parser, options):
3183 _process_codereview_select_options(parser, options)
3184 if options.issue is not None and not options.forced_codereview:
3185 parser.error('--issue must be specified with either --rietveld or --gerrit')
3186
3187
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003188def _add_codereview_select_options(parser):
3189 """Appends --gerrit and --rietveld options to force specific codereview."""
3190 parser.codereview_group = optparse.OptionGroup(
3191 parser, 'EXPERIMENTAL! Codereview override options')
3192 parser.add_option_group(parser.codereview_group)
3193 parser.codereview_group.add_option(
3194 '--gerrit', action='store_true',
3195 help='Force the use of Gerrit for codereview')
3196 parser.codereview_group.add_option(
3197 '--rietveld', action='store_true',
3198 help='Force the use of Rietveld for codereview')
3199
3200
3201def _process_codereview_select_options(parser, options):
3202 if options.gerrit and options.rietveld:
3203 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3204 options.forced_codereview = None
3205 if options.gerrit:
3206 options.forced_codereview = 'gerrit'
3207 elif options.rietveld:
3208 options.forced_codereview = 'rietveld'
3209
3210
tandriif9aefb72016-07-01 09:06:51 -07003211def _get_bug_line_values(default_project, bugs):
3212 """Given default_project and comma separated list of bugs, yields bug line
3213 values.
3214
3215 Each bug can be either:
3216 * a number, which is combined with default_project
3217 * string, which is left as is.
3218
3219 This function may produce more than one line, because bugdroid expects one
3220 project per line.
3221
3222 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3223 ['v8:123', 'chromium:789']
3224 """
3225 default_bugs = []
3226 others = []
3227 for bug in bugs.split(','):
3228 bug = bug.strip()
3229 if bug:
3230 try:
3231 default_bugs.append(int(bug))
3232 except ValueError:
3233 others.append(bug)
3234
3235 if default_bugs:
3236 default_bugs = ','.join(map(str, default_bugs))
3237 if default_project:
3238 yield '%s:%s' % (default_project, default_bugs)
3239 else:
3240 yield default_bugs
3241 for other in sorted(others):
3242 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3243 yield other
3244
3245
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003246class ChangeDescription(object):
3247 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003248 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003249 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003250 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003251 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003252
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003253 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003254 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003255
agable@chromium.org42c20792013-09-12 17:34:49 +00003256 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003257 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003258 return '\n'.join(self._description_lines)
3259
3260 def set_description(self, desc):
3261 if isinstance(desc, basestring):
3262 lines = desc.splitlines()
3263 else:
3264 lines = [line.rstrip() for line in desc]
3265 while lines and not lines[0]:
3266 lines.pop(0)
3267 while lines and not lines[-1]:
3268 lines.pop(-1)
3269 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003270
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003271 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3272 """Rewrites the R=/TBR= line(s) as a single line each.
3273
3274 Args:
3275 reviewers (list(str)) - list of additional emails to use for reviewers.
3276 tbrs (list(str)) - list of additional emails to use for TBRs.
3277 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3278 the change that are missing OWNER coverage. If this is not None, you
3279 must also pass a value for `change`.
3280 change (Change) - The Change that should be used for OWNERS lookups.
3281 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003282 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003283 assert isinstance(tbrs, list), tbrs
3284
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003285 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003286 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003287
3288 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003289 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003290
3291 reviewers = set(reviewers)
3292 tbrs = set(tbrs)
3293 LOOKUP = {
3294 'TBR': tbrs,
3295 'R': reviewers,
3296 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003297
agable@chromium.org42c20792013-09-12 17:34:49 +00003298 # Get the set of R= and TBR= lines and remove them from the desciption.
3299 regexp = re.compile(self.R_LINE)
3300 matches = [regexp.match(line) for line in self._description_lines]
3301 new_desc = [l for i, l in enumerate(self._description_lines)
3302 if not matches[i]]
3303 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003304
agable@chromium.org42c20792013-09-12 17:34:49 +00003305 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003306
3307 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003308 for match in matches:
3309 if not match:
3310 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003311 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3312
3313 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003314 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003315 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003316 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003317 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003318 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003319 LOOKUP[add_owners_to].update(
3320 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003321
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003322 # If any folks ended up in both groups, remove them from tbrs.
3323 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003324
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003325 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3326 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003327
3328 # Put the new lines in the description where the old first R= line was.
3329 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3330 if 0 <= line_loc < len(self._description_lines):
3331 if new_tbr_line:
3332 self._description_lines.insert(line_loc, new_tbr_line)
3333 if new_r_line:
3334 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003335 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003336 if new_r_line:
3337 self.append_footer(new_r_line)
3338 if new_tbr_line:
3339 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003340
Aaron Gable3a16ed12017-03-23 10:51:55 -07003341 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003342 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003343 self.set_description([
3344 '# Enter a description of the change.',
3345 '# This will be displayed on the codereview site.',
3346 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003347 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003348 '--------------------',
3349 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003350
agable@chromium.org42c20792013-09-12 17:34:49 +00003351 regexp = re.compile(self.BUG_LINE)
3352 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003353 prefix = settings.GetBugPrefix()
3354 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003355 if git_footer:
3356 self.append_footer('Bug: %s' % ', '.join(values))
3357 else:
3358 for value in values:
3359 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003360
agable@chromium.org42c20792013-09-12 17:34:49 +00003361 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003362 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003363 if not content:
3364 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003365 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003366
3367 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003368 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3369 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003370 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003371 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003372
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003373 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003374 """Adds a footer line to the description.
3375
3376 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3377 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3378 that Gerrit footers are always at the end.
3379 """
3380 parsed_footer_line = git_footers.parse_footer(line)
3381 if parsed_footer_line:
3382 # Line is a gerrit footer in the form: Footer-Key: any value.
3383 # Thus, must be appended observing Gerrit footer rules.
3384 self.set_description(
3385 git_footers.add_footer(self.description,
3386 key=parsed_footer_line[0],
3387 value=parsed_footer_line[1]))
3388 return
3389
3390 if not self._description_lines:
3391 self._description_lines.append(line)
3392 return
3393
3394 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3395 if gerrit_footers:
3396 # git_footers.split_footers ensures that there is an empty line before
3397 # actual (gerrit) footers, if any. We have to keep it that way.
3398 assert top_lines and top_lines[-1] == ''
3399 top_lines, separator = top_lines[:-1], top_lines[-1:]
3400 else:
3401 separator = [] # No need for separator if there are no gerrit_footers.
3402
3403 prev_line = top_lines[-1] if top_lines else ''
3404 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3405 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3406 top_lines.append('')
3407 top_lines.append(line)
3408 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003409
tandrii99a72f22016-08-17 14:33:24 -07003410 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003411 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003412 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003413 reviewers = [match.group(2).strip()
3414 for match in matches
3415 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003416 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003417
bradnelsond975b302016-10-23 12:20:23 -07003418 def get_cced(self):
3419 """Retrieves the list of reviewers."""
3420 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3421 cced = [match.group(2).strip() for match in matches if match]
3422 return cleanup_list(cced)
3423
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003424 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3425 """Updates this commit description given the parent.
3426
3427 This is essentially what Gnumbd used to do.
3428 Consult https://goo.gl/WMmpDe for more details.
3429 """
3430 assert parent_msg # No, orphan branch creation isn't supported.
3431 assert parent_hash
3432 assert dest_ref
3433 parent_footer_map = git_footers.parse_footers(parent_msg)
3434 # This will also happily parse svn-position, which GnumbD is no longer
3435 # supporting. While we'd generate correct footers, the verifier plugin
3436 # installed in Gerrit will block such commit (ie git push below will fail).
3437 parent_position = git_footers.get_position(parent_footer_map)
3438
3439 # Cherry-picks may have last line obscuring their prior footers,
3440 # from git_footers perspective. This is also what Gnumbd did.
3441 cp_line = None
3442 if (self._description_lines and
3443 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3444 cp_line = self._description_lines.pop()
3445
3446 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3447
3448 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3449 # user interference with actual footers we'd insert below.
3450 for i, (k, v) in enumerate(parsed_footers):
3451 if k.startswith('Cr-'):
3452 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3453
3454 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003455 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003456 if parent_position[0] == dest_ref:
3457 # Same branch as parent.
3458 number = int(parent_position[1]) + 1
3459 else:
3460 number = 1 # New branch, and extra lineage.
3461 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3462 int(parent_position[1])))
3463
3464 parsed_footers.append(('Cr-Commit-Position',
3465 '%s@{#%d}' % (dest_ref, number)))
3466 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3467
3468 self._description_lines = top_lines
3469 if cp_line:
3470 self._description_lines.append(cp_line)
3471 if self._description_lines[-1] != '':
3472 self._description_lines.append('') # Ensure footer separator.
3473 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3474
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003475
Aaron Gablea1bab272017-04-11 16:38:18 -07003476def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003477 """Retrieves the reviewers that approved a CL from the issue properties with
3478 messages.
3479
3480 Note that the list may contain reviewers that are not committer, thus are not
3481 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003482
3483 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003484 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003485 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003486 return sorted(
3487 set(
3488 message['sender']
3489 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003490 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003491 )
3492 )
3493
3494
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003495def FindCodereviewSettingsFile(filename='codereview.settings'):
3496 """Finds the given file starting in the cwd and going up.
3497
3498 Only looks up to the top of the repository unless an
3499 'inherit-review-settings-ok' file exists in the root of the repository.
3500 """
3501 inherit_ok_file = 'inherit-review-settings-ok'
3502 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003503 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003504 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3505 root = '/'
3506 while True:
3507 if filename in os.listdir(cwd):
3508 if os.path.isfile(os.path.join(cwd, filename)):
3509 return open(os.path.join(cwd, filename))
3510 if cwd == root:
3511 break
3512 cwd = os.path.dirname(cwd)
3513
3514
3515def LoadCodereviewSettingsFromFile(fileobj):
3516 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003517 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003518
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003519 def SetProperty(name, setting, unset_error_ok=False):
3520 fullname = 'rietveld.' + name
3521 if setting in keyvals:
3522 RunGit(['config', fullname, keyvals[setting]])
3523 else:
3524 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3525
tandrii48df5812016-10-17 03:55:37 -07003526 if not keyvals.get('GERRIT_HOST', False):
3527 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003528 # Only server setting is required. Other settings can be absent.
3529 # In that case, we ignore errors raised during option deletion attempt.
3530 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003531 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003532 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3533 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003534 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003535 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3536 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003537 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003538 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3539 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003540
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003541 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003542 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003543
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003544 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003545 RunGit(['config', 'gerrit.squash-uploads',
3546 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003547
tandrii@chromium.org28253532016-04-14 13:46:56 +00003548 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003549 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003550 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3551
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003552 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003553 # should be of the form
3554 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3555 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003556 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3557 keyvals['ORIGIN_URL_CONFIG']])
3558
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003559
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003560def urlretrieve(source, destination):
3561 """urllib is broken for SSL connections via a proxy therefore we
3562 can't use urllib.urlretrieve()."""
3563 with open(destination, 'w') as f:
3564 f.write(urllib2.urlopen(source).read())
3565
3566
ukai@chromium.org712d6102013-11-27 00:52:58 +00003567def hasSheBang(fname):
3568 """Checks fname is a #! script."""
3569 with open(fname) as f:
3570 return f.read(2).startswith('#!')
3571
3572
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003573# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3574def DownloadHooks(*args, **kwargs):
3575 pass
3576
3577
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003578def DownloadGerritHook(force):
3579 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003580
3581 Args:
3582 force: True to update hooks. False to install hooks if not present.
3583 """
3584 if not settings.GetIsGerrit():
3585 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003586 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003587 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3588 if not os.access(dst, os.X_OK):
3589 if os.path.exists(dst):
3590 if not force:
3591 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003592 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003593 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003594 if not hasSheBang(dst):
3595 DieWithError('Not a script: %s\n'
3596 'You need to download from\n%s\n'
3597 'into .git/hooks/commit-msg and '
3598 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003599 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3600 except Exception:
3601 if os.path.exists(dst):
3602 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003603 DieWithError('\nFailed to download hooks.\n'
3604 'You need to download from\n%s\n'
3605 'into .git/hooks/commit-msg and '
3606 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003607
3608
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003609def GetRietveldCodereviewSettingsInteractively():
3610 """Prompt the user for settings."""
3611 server = settings.GetDefaultServerUrl(error_ok=True)
3612 prompt = 'Rietveld server (host[:port])'
3613 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3614 newserver = ask_for_data(prompt + ':')
3615 if not server and not newserver:
3616 newserver = DEFAULT_SERVER
3617 if newserver:
3618 newserver = gclient_utils.UpgradeToHttps(newserver)
3619 if newserver != server:
3620 RunGit(['config', 'rietveld.server', newserver])
3621
3622 def SetProperty(initial, caption, name, is_url):
3623 prompt = caption
3624 if initial:
3625 prompt += ' ("x" to clear) [%s]' % initial
3626 new_val = ask_for_data(prompt + ':')
3627 if new_val == 'x':
3628 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3629 elif new_val:
3630 if is_url:
3631 new_val = gclient_utils.UpgradeToHttps(new_val)
3632 if new_val != initial:
3633 RunGit(['config', 'rietveld.' + name, new_val])
3634
3635 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3636 SetProperty(settings.GetDefaultPrivateFlag(),
3637 'Private flag (rietveld only)', 'private', False)
3638 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3639 'tree-status-url', False)
3640 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3641 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3642 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3643 'run-post-upload-hook', False)
3644
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003645
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003646class _GitCookiesChecker(object):
3647 """Provides facilties for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003648
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003649 _GOOGLESOURCE = 'googlesource.com'
3650
3651 def __init__(self):
3652 # Cached list of [host, identity, source], where source is either
3653 # .gitcookies or .netrc.
3654 self._all_hosts = None
3655
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003656 def ensure_configured_gitcookies(self):
3657 """Runs checks and suggests fixes to make git use .gitcookies from default
3658 path."""
3659 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3660 configured_path = RunGitSilent(
3661 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003662 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003663 if configured_path:
3664 self._ensure_default_gitcookies_path(configured_path, default)
3665 else:
3666 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003667
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003668 @staticmethod
3669 def _ensure_default_gitcookies_path(configured_path, default_path):
3670 assert configured_path
3671 if configured_path == default_path:
3672 print('git is already configured to use your .gitcookies from %s' %
3673 configured_path)
3674 return
3675
3676 print('WARNING: you have configured custom path to .gitcookies: %s\n'
3677 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3678 (configured_path, default_path))
3679
3680 if not os.path.exists(configured_path):
3681 print('However, your configured .gitcookies file is missing.')
3682 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3683 action='reconfigure')
3684 RunGit(['config', '--global', 'http.cookiefile', default_path])
3685 return
3686
3687 if os.path.exists(default_path):
3688 print('WARNING: default .gitcookies file already exists %s' %
3689 default_path)
3690 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3691 default_path)
3692
3693 confirm_or_exit('Move existing .gitcookies to default location?',
3694 action='move')
3695 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003696 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003697 print('Moved and reconfigured git to use .gitcookies from %s' %
3698 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003699
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003700 @staticmethod
3701 def _configure_gitcookies_path(default_path):
3702 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3703 if os.path.exists(netrc_path):
3704 print('You seem to be using outdated .netrc for git credentials: %s' %
3705 netrc_path)
3706 print('This tool will guide you through setting up recommended '
3707 '.gitcookies store for git credentials.\n'
3708 '\n'
3709 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3710 ' git config --global --unset http.cookiefile\n'
3711 ' mv %s %s.backup\n\n' % (default_path, default_path))
3712 confirm_or_exit(action='setup .gitcookies')
3713 RunGit(['config', '--global', 'http.cookiefile', default_path])
3714 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003715
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003716 def get_hosts_with_creds(self, include_netrc=False):
3717 if self._all_hosts is None:
3718 a = gerrit_util.CookiesAuthenticator()
3719 self._all_hosts = [
3720 (h, u, s)
3721 for h, u, s in itertools.chain(
3722 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3723 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3724 )
3725 if h.endswith(self._GOOGLESOURCE)
3726 ]
3727
3728 if include_netrc:
3729 return self._all_hosts
3730 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3731
3732 def print_current_creds(self, include_netrc=False):
3733 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3734 if not hosts:
3735 print('No Git/Gerrit credentials found')
3736 return
3737 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3738 header = [('Host', 'User', 'Which file'),
3739 ['=' * l for l in lengths]]
3740 for row in (header + hosts):
3741 print('\t'.join((('%%+%ds' % l) % s)
3742 for l, s in zip(lengths, row)))
3743
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003744 @staticmethod
3745 def _parse_identity(identity):
3746 """Parses identity "git-<ldap>.example.com" into <ldap> and domain."""
3747 username, domain = identity.split('.', 1)
3748 if username.startswith('git-'):
3749 username = username[len('git-'):]
3750 return username, domain
3751
3752 def _get_usernames_of_domain(self, domain):
3753 """Returns list of usernames referenced by .gitcookies in a given domain."""
3754 identities_by_domain = {}
3755 for _, identity, _ in self.get_hosts_with_creds():
3756 username, domain = self._parse_identity(identity)
3757 identities_by_domain.setdefault(domain, []).append(username)
3758 return identities_by_domain.get(domain)
3759
3760 def _canonical_git_googlesource_host(self, host):
3761 """Normalizes Gerrit hosts (with '-review') to Git host."""
3762 assert host.endswith(self._GOOGLESOURCE)
3763 # Prefix doesn't include '.' at the end.
3764 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3765 if prefix.endswith('-review'):
3766 prefix = prefix[:-len('-review')]
3767 return prefix + '.' + self._GOOGLESOURCE
3768
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003769 def _canonical_gerrit_googlesource_host(self, host):
3770 git_host = self._canonical_git_googlesource_host(host)
3771 prefix = git_host.split('.', 1)[0]
3772 return prefix + '-review.' + self._GOOGLESOURCE
3773
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003774 def has_generic_host(self):
3775 """Returns whether generic .googlesource.com has been configured.
3776
3777 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3778 """
3779 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3780 if host == '.' + self._GOOGLESOURCE:
3781 return True
3782 return False
3783
3784 def _get_git_gerrit_identity_pairs(self):
3785 """Returns map from canonic host to pair of identities (Git, Gerrit).
3786
3787 One of identities might be None, meaning not configured.
3788 """
3789 host_to_identity_pairs = {}
3790 for host, identity, _ in self.get_hosts_with_creds():
3791 canonical = self._canonical_git_googlesource_host(host)
3792 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3793 idx = 0 if canonical == host else 1
3794 pair[idx] = identity
3795 return host_to_identity_pairs
3796
3797 def get_partially_configured_hosts(self):
3798 return set(
3799 host for host, identities_pair in
3800 self._get_git_gerrit_identity_pairs().iteritems()
3801 if None in identities_pair and host != '.' + self._GOOGLESOURCE)
3802
3803 def get_conflicting_hosts(self):
3804 return set(
3805 host for host, (i1, i2) in
3806 self._get_git_gerrit_identity_pairs().iteritems()
3807 if None not in (i1, i2) and i1 != i2)
3808
3809 def get_duplicated_hosts(self):
3810 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3811 return set(host for host, count in counters.iteritems() if count > 1)
3812
3813 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3814 'chromium.googlesource.com': 'chromium.org',
3815 'chrome-internal.googlesource.com': 'google.com',
3816 }
3817
3818 def get_hosts_with_wrong_identities(self):
3819 """Finds hosts which **likely** reference wrong identities.
3820
3821 Note: skips hosts which have conflicting identities for Git and Gerrit.
3822 """
3823 hosts = set()
3824 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3825 pair = self._get_git_gerrit_identity_pairs().get(host)
3826 if pair and pair[0] == pair[1]:
3827 _, domain = self._parse_identity(pair[0])
3828 if domain != expected:
3829 hosts.add(host)
3830 return hosts
3831
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003832 @staticmethod
3833 def print_hosts(hosts, extra_column_func=None):
3834 hosts = sorted(hosts)
3835 assert hosts
3836 if extra_column_func is None:
3837 extras = [''] * len(hosts)
3838 else:
3839 extras = [extra_column_func(host) for host in hosts]
3840 tmpl = ' %%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3841 for he in zip(hosts, extras):
3842 print(tmpl % he)
3843 print()
3844
3845 def find_and_report_problems(self):
3846 """Returns True if there was at least one problem, else False."""
3847 problems = [False]
3848 def add_problem():
3849 if not problems[0]:
Andrii Shyshkalov4812e612017-03-27 17:22:57 +02003850 print('\n\n.gitcookies problem report:\n')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003851 problems[0] = True
3852
3853 if self.has_generic_host():
3854 add_problem()
3855 print(' .googlesource.com record detected\n'
3856 ' Chrome Infrastructure team recommends to list full host names '
3857 'explicitly.\n')
3858
3859 dups = self.get_duplicated_hosts()
3860 if dups:
3861 add_problem()
3862 print(' The following hosts were defined twice:\n')
3863 self.print_hosts(dups)
3864
3865 partial = self.get_partially_configured_hosts()
3866 if partial:
3867 add_problem()
3868 print(' Credentials should come in pairs for Git and Gerrit hosts. '
3869 'These hosts are missing:')
3870 self.print_hosts(partial)
3871
3872 conflicting = self.get_conflicting_hosts()
3873 if conflicting:
3874 add_problem()
3875 print(' The following Git hosts have differing credentials from their '
3876 'Gerrit counterparts:\n')
3877 self.print_hosts(conflicting, lambda host: '%s vs %s' %
3878 tuple(self._get_git_gerrit_identity_pairs()[host]))
3879
3880 wrong = self.get_hosts_with_wrong_identities()
3881 if wrong:
3882 add_problem()
3883 print(' These hosts likely use wrong identity:\n')
3884 self.print_hosts(wrong, lambda host: '%s but %s recommended' %
3885 (self._get_git_gerrit_identity_pairs()[host][0],
3886 self._EXPECTED_HOST_IDENTITY_DOMAINS[host]))
3887 return problems[0]
3888
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003889
3890def CMDcreds_check(parser, args):
3891 """Checks credentials and suggests changes."""
3892 _, _ = parser.parse_args(args)
3893
3894 if gerrit_util.GceAuthenticator.is_gce():
3895 DieWithError('this command is not designed for GCE, are you on a bot?')
3896
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003897 checker = _GitCookiesChecker()
3898 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003899
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003900 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003901 checker.print_current_creds(include_netrc=True)
3902
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003903 if not checker.find_and_report_problems():
3904 print('\nNo problems detected in your .gitcookies')
3905 return 0
3906 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003907
3908
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003909@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003910def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003911 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003912
tandrii5d0a0422016-09-14 06:24:35 -07003913 print('WARNING: git cl config works for Rietveld only')
3914 # TODO(tandrii): remove this once we switch to Gerrit.
3915 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003916 parser.add_option('--activate-update', action='store_true',
3917 help='activate auto-updating [rietveld] section in '
3918 '.git/config')
3919 parser.add_option('--deactivate-update', action='store_true',
3920 help='deactivate auto-updating [rietveld] section in '
3921 '.git/config')
3922 options, args = parser.parse_args(args)
3923
3924 if options.deactivate_update:
3925 RunGit(['config', 'rietveld.autoupdate', 'false'])
3926 return
3927
3928 if options.activate_update:
3929 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3930 return
3931
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003932 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003933 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003934 return 0
3935
3936 url = args[0]
3937 if not url.endswith('codereview.settings'):
3938 url = os.path.join(url, 'codereview.settings')
3939
3940 # Load code review settings and download hooks (if available).
3941 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3942 return 0
3943
3944
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003945def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003946 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003947 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3948 branch = ShortBranchName(branchref)
3949 _, args = parser.parse_args(args)
3950 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003951 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003952 return RunGit(['config', 'branch.%s.base-url' % branch],
3953 error_ok=False).strip()
3954 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003955 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003956 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3957 error_ok=False).strip()
3958
3959
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003960def color_for_status(status):
3961 """Maps a Changelist status to color, for CMDstatus and other tools."""
3962 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003963 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003964 'waiting': Fore.BLUE,
3965 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003966 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003967 'lgtm': Fore.GREEN,
3968 'commit': Fore.MAGENTA,
3969 'closed': Fore.CYAN,
3970 'error': Fore.WHITE,
3971 }.get(status, Fore.WHITE)
3972
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003973
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003974def get_cl_statuses(changes, fine_grained, max_processes=None):
3975 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003976
3977 If fine_grained is true, this will fetch CL statuses from the server.
3978 Otherwise, simply indicate if there's a matching url for the given branches.
3979
3980 If max_processes is specified, it is used as the maximum number of processes
3981 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3982 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003983
3984 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003985 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003986 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003987 upload.verbosity = 0
3988
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003989 if not changes:
3990 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003991
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003992 if not fine_grained:
3993 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003994 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003995 for cl in changes:
3996 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003997 return
3998
3999 # First, sort out authentication issues.
4000 logging.debug('ensuring credentials exist')
4001 for cl in changes:
4002 cl.EnsureAuthenticated(force=False, refresh=True)
4003
4004 def fetch(cl):
4005 try:
4006 return (cl, cl.GetStatus())
4007 except:
4008 # See http://crbug.com/629863.
4009 logging.exception('failed to fetch status for %s:', cl)
4010 raise
4011
4012 threads_count = len(changes)
4013 if max_processes:
4014 threads_count = max(1, min(threads_count, max_processes))
4015 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4016
4017 pool = ThreadPool(threads_count)
4018 fetched_cls = set()
4019 try:
4020 it = pool.imap_unordered(fetch, changes).__iter__()
4021 while True:
4022 try:
4023 cl, status = it.next(timeout=5)
4024 except multiprocessing.TimeoutError:
4025 break
4026 fetched_cls.add(cl)
4027 yield cl, status
4028 finally:
4029 pool.close()
4030
4031 # Add any branches that failed to fetch.
4032 for cl in set(changes) - fetched_cls:
4033 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004034
rmistry@google.com2dd99862015-06-22 12:22:18 +00004035
4036def upload_branch_deps(cl, args):
4037 """Uploads CLs of local branches that are dependents of the current branch.
4038
4039 If the local branch dependency tree looks like:
4040 test1 -> test2.1 -> test3.1
4041 -> test3.2
4042 -> test2.2 -> test3.3
4043
4044 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4045 run on the dependent branches in this order:
4046 test2.1, test3.1, test3.2, test2.2, test3.3
4047
4048 Note: This function does not rebase your local dependent branches. Use it when
4049 you make a change to the parent branch that will not conflict with its
4050 dependent branches, and you would like their dependencies updated in
4051 Rietveld.
4052 """
4053 if git_common.is_dirty_git_tree('upload-branch-deps'):
4054 return 1
4055
4056 root_branch = cl.GetBranch()
4057 if root_branch is None:
4058 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4059 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004060 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004061 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4062 'patchset dependencies without an uploaded CL.')
4063
4064 branches = RunGit(['for-each-ref',
4065 '--format=%(refname:short) %(upstream:short)',
4066 'refs/heads'])
4067 if not branches:
4068 print('No local branches found.')
4069 return 0
4070
4071 # Create a dictionary of all local branches to the branches that are dependent
4072 # on it.
4073 tracked_to_dependents = collections.defaultdict(list)
4074 for b in branches.splitlines():
4075 tokens = b.split()
4076 if len(tokens) == 2:
4077 branch_name, tracked = tokens
4078 tracked_to_dependents[tracked].append(branch_name)
4079
vapiera7fbd5a2016-06-16 09:17:49 -07004080 print()
4081 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004082 dependents = []
4083 def traverse_dependents_preorder(branch, padding=''):
4084 dependents_to_process = tracked_to_dependents.get(branch, [])
4085 padding += ' '
4086 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004087 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004088 dependents.append(dependent)
4089 traverse_dependents_preorder(dependent, padding)
4090 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004091 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004092
4093 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004094 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004095 return 0
4096
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004097 confirm_or_exit('This command will checkout all dependent branches and run '
4098 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004099
andybons@chromium.org962f9462016-02-03 20:00:42 +00004100 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004101 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004102 args.extend(['-t', 'Updated patchset dependency'])
4103
rmistry@google.com2dd99862015-06-22 12:22:18 +00004104 # Record all dependents that failed to upload.
4105 failures = {}
4106 # Go through all dependents, checkout the branch and upload.
4107 try:
4108 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004109 print()
4110 print('--------------------------------------')
4111 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004112 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004113 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004114 try:
4115 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004116 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004117 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004118 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004119 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004120 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004121 finally:
4122 # Swap back to the original root branch.
4123 RunGit(['checkout', '-q', root_branch])
4124
vapiera7fbd5a2016-06-16 09:17:49 -07004125 print()
4126 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004127 for dependent_branch in dependents:
4128 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004129 print(' %s : %s' % (dependent_branch, upload_status))
4130 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004131
4132 return 0
4133
4134
kmarshall3bff56b2016-06-06 18:31:47 -07004135def CMDarchive(parser, args):
4136 """Archives and deletes branches associated with closed changelists."""
4137 parser.add_option(
4138 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004139 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004140 parser.add_option(
4141 '-f', '--force', action='store_true',
4142 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004143 parser.add_option(
4144 '-d', '--dry-run', action='store_true',
4145 help='Skip the branch tagging and removal steps.')
4146 parser.add_option(
4147 '-t', '--notags', action='store_true',
4148 help='Do not tag archived branches. '
4149 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004150
4151 auth.add_auth_options(parser)
4152 options, args = parser.parse_args(args)
4153 if args:
4154 parser.error('Unsupported args: %s' % ' '.join(args))
4155 auth_config = auth.extract_auth_config_from_options(options)
4156
4157 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4158 if not branches:
4159 return 0
4160
vapiera7fbd5a2016-06-16 09:17:49 -07004161 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004162 changes = [Changelist(branchref=b, auth_config=auth_config)
4163 for b in branches.splitlines()]
4164 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4165 statuses = get_cl_statuses(changes,
4166 fine_grained=True,
4167 max_processes=options.maxjobs)
4168 proposal = [(cl.GetBranch(),
4169 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4170 for cl, status in statuses
4171 if status == 'closed']
4172 proposal.sort()
4173
4174 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004175 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004176 return 0
4177
4178 current_branch = GetCurrentBranch()
4179
vapiera7fbd5a2016-06-16 09:17:49 -07004180 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004181 if options.notags:
4182 for next_item in proposal:
4183 print(' ' + next_item[0])
4184 else:
4185 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4186 for next_item in proposal:
4187 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004188
kmarshall9249e012016-08-23 12:02:16 -07004189 # Quit now on precondition failure or if instructed by the user, either
4190 # via an interactive prompt or by command line flags.
4191 if options.dry_run:
4192 print('\nNo changes were made (dry run).\n')
4193 return 0
4194 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004195 print('You are currently on a branch \'%s\' which is associated with a '
4196 'closed codereview issue, so archive cannot proceed. Please '
4197 'checkout another branch and run this command again.' %
4198 current_branch)
4199 return 1
kmarshall9249e012016-08-23 12:02:16 -07004200 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004201 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4202 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004203 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004204 return 1
4205
4206 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004207 if not options.notags:
4208 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004209 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004210
vapiera7fbd5a2016-06-16 09:17:49 -07004211 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004212
4213 return 0
4214
4215
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004216def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004217 """Show status of changelists.
4218
4219 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004220 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004221 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004222 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004223 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004224 - Magenta in the commit queue
4225 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004226 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004227
4228 Also see 'git cl comments'.
4229 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004230 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004231 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004232 parser.add_option('-f', '--fast', action='store_true',
4233 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004234 parser.add_option(
4235 '-j', '--maxjobs', action='store', type=int,
4236 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004237
4238 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004239 _add_codereview_issue_select_options(
4240 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004241 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004242 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004243 if args:
4244 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004245 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004246
iannuccie53c9352016-08-17 14:40:40 -07004247 if options.issue is not None and not options.field:
4248 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004249
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004250 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004251 cl = Changelist(auth_config=auth_config, issue=options.issue,
4252 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004253 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004254 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004255 elif options.field == 'id':
4256 issueid = cl.GetIssue()
4257 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004258 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004259 elif options.field == 'patch':
4260 patchset = cl.GetPatchset()
4261 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004262 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004263 elif options.field == 'status':
4264 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004265 elif options.field == 'url':
4266 url = cl.GetIssueURL()
4267 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004268 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004269 return 0
4270
4271 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4272 if not branches:
4273 print('No local branch found.')
4274 return 0
4275
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004276 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004277 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004278 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004279 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004280 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004281 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004282 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004283
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004284 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004285 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4286 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4287 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004288 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004289 c, status = output.next()
4290 branch_statuses[c.GetBranch()] = status
4291 status = branch_statuses.pop(branch)
4292 url = cl.GetIssueURL()
4293 if url and (not status or status == 'error'):
4294 # The issue probably doesn't exist anymore.
4295 url += ' (broken)'
4296
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004297 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004298 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004299 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004300 color = ''
4301 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004302 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004303 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004304 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004305 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004306
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004307
4308 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004309 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004310 print('Current branch: %s' % branch)
4311 for cl in changes:
4312 if cl.GetBranch() == branch:
4313 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004314 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004315 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004316 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004317 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004318 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004319 print('Issue description:')
4320 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004321 return 0
4322
4323
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004324def colorize_CMDstatus_doc():
4325 """To be called once in main() to add colors to git cl status help."""
4326 colors = [i for i in dir(Fore) if i[0].isupper()]
4327
4328 def colorize_line(line):
4329 for color in colors:
4330 if color in line.upper():
4331 # Extract whitespaces first and the leading '-'.
4332 indent = len(line) - len(line.lstrip(' ')) + 1
4333 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4334 return line
4335
4336 lines = CMDstatus.__doc__.splitlines()
4337 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4338
4339
phajdan.jre328cf92016-08-22 04:12:17 -07004340def write_json(path, contents):
4341 with open(path, 'w') as f:
4342 json.dump(contents, f)
4343
4344
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004345@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004346def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004347 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004348
4349 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004350 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004351 parser.add_option('-r', '--reverse', action='store_true',
4352 help='Lookup the branch(es) for the specified issues. If '
4353 'no issues are specified, all branches with mapped '
4354 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07004355 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004356 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004357 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004358 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004359
dnj@chromium.org406c4402015-03-03 17:22:28 +00004360 if options.reverse:
4361 branches = RunGit(['for-each-ref', 'refs/heads',
4362 '--format=%(refname:short)']).splitlines()
4363
4364 # Reverse issue lookup.
4365 issue_branch_map = {}
4366 for branch in branches:
4367 cl = Changelist(branchref=branch)
4368 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4369 if not args:
4370 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004371 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004372 for issue in args:
4373 if not issue:
4374 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004375 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004376 print('Branch for issue number %s: %s' % (
4377 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004378 if options.json:
4379 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004380 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004381 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004382 if len(args) > 0:
4383 try:
4384 issue = int(args[0])
4385 except ValueError:
4386 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00004387 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00004388 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07004389 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07004390 if options.json:
4391 write_json(options.json, {
4392 'issue': cl.GetIssue(),
4393 'issue_url': cl.GetIssueURL(),
4394 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004395 return 0
4396
4397
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004398def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004399 """Shows or posts review comments for any changelist."""
4400 parser.add_option('-a', '--add-comment', dest='comment',
4401 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004402 parser.add_option('-i', '--issue', dest='issue',
4403 help='review issue id (defaults to current issue). '
4404 'If given, requires --rietveld or --gerrit')
smut@google.comc85ac942015-09-15 16:34:43 +00004405 parser.add_option('-j', '--json-file',
4406 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004407 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004408 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004409 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004410 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004411 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004412
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004413 issue = None
4414 if options.issue:
4415 try:
4416 issue = int(options.issue)
4417 except ValueError:
4418 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004419 if not options.forced_codereview:
4420 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004421
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004422 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004423 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004424 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004425
4426 if options.comment:
4427 cl.AddComment(options.comment)
4428 return 0
4429
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004430 summary = sorted(cl.GetCommentsSummary(), key=lambda c: c.date)
4431 for comment in summary:
4432 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004433 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004434 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004435 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004436 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004437 color = Fore.MAGENTA
4438 else:
4439 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004440 print('\n%s%s %s%s\n%s' % (
4441 color,
4442 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4443 comment.sender,
4444 Fore.RESET,
4445 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4446
smut@google.comc85ac942015-09-15 16:34:43 +00004447 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004448 def pre_serialize(c):
4449 dct = c.__dict__.copy()
4450 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4451 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004452 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004453 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004454 return 0
4455
4456
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004457@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004458def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004459 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004460 parser.add_option('-d', '--display', action='store_true',
4461 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004462 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004463 help='New description to set for this issue (- for stdin, '
4464 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004465 parser.add_option('-f', '--force', action='store_true',
4466 help='Delete any unpublished Gerrit edits for this issue '
4467 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004468
4469 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004470 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004471 options, args = parser.parse_args(args)
4472 _process_codereview_select_options(parser, options)
4473
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004474 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004475 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004476 target_issue_arg = ParseIssueNumberArgument(args[0],
4477 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004478 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004479 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004480
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004481 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004482
martiniss6eda05f2016-06-30 10:18:35 -07004483 kwargs = {
4484 'auth_config': auth_config,
4485 'codereview': options.forced_codereview,
4486 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004487 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004488 if target_issue_arg:
4489 kwargs['issue'] = target_issue_arg.issue
4490 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004491 if target_issue_arg.codereview and not options.forced_codereview:
4492 detected_codereview_from_url = True
4493 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004494
4495 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004496 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004497 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004498 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004499
4500 if detected_codereview_from_url:
4501 logging.info('canonical issue/change URL: %s (type: %s)\n',
4502 cl.GetIssueURL(), target_issue_arg.codereview)
4503
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004504 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004505
smut@google.com34fb6b12015-07-13 20:03:26 +00004506 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004507 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004508 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004509
4510 if options.new_description:
4511 text = options.new_description
4512 if text == '-':
4513 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004514 elif text == '+':
4515 base_branch = cl.GetCommonAncestorWithUpstream()
4516 change = cl.GetChange(base_branch, None, local_description=True)
4517 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004518
4519 description.set_description(text)
4520 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004521 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004522
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004523 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004524 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004525 return 0
4526
4527
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004528def CreateDescriptionFromLog(args):
4529 """Pulls out the commit log to use as a base for the CL description."""
4530 log_args = []
4531 if len(args) == 1 and not args[0].endswith('.'):
4532 log_args = [args[0] + '..']
4533 elif len(args) == 1 and args[0].endswith('...'):
4534 log_args = [args[0][:-1]]
4535 elif len(args) == 2:
4536 log_args = [args[0] + '..' + args[1]]
4537 else:
4538 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004539 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004540
4541
thestig@chromium.org44202a22014-03-11 19:22:18 +00004542def CMDlint(parser, args):
4543 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004544 parser.add_option('--filter', action='append', metavar='-x,+y',
4545 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004546 auth.add_auth_options(parser)
4547 options, args = parser.parse_args(args)
4548 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004549
4550 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004551 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004552 try:
4553 import cpplint
4554 import cpplint_chromium
4555 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004556 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004557 return 1
4558
4559 # Change the current working directory before calling lint so that it
4560 # shows the correct base.
4561 previous_cwd = os.getcwd()
4562 os.chdir(settings.GetRoot())
4563 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004564 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004565 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4566 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004567 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004568 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004569 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004570
4571 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004572 command = args + files
4573 if options.filter:
4574 command = ['--filter=' + ','.join(options.filter)] + command
4575 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004576
4577 white_regex = re.compile(settings.GetLintRegex())
4578 black_regex = re.compile(settings.GetLintIgnoreRegex())
4579 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4580 for filename in filenames:
4581 if white_regex.match(filename):
4582 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004583 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004584 else:
4585 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4586 extra_check_functions)
4587 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004588 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004589 finally:
4590 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004591 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004592 if cpplint._cpplint_state.error_count != 0:
4593 return 1
4594 return 0
4595
4596
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004597def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004598 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004599 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004600 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004601 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004602 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004603 auth.add_auth_options(parser)
4604 options, args = parser.parse_args(args)
4605 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004606
sbc@chromium.org71437c02015-04-09 19:29:40 +00004607 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004608 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004609 return 1
4610
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004611 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004612 if args:
4613 base_branch = args[0]
4614 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004615 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004616 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004617
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004618 cl.RunHook(
4619 committing=not options.upload,
4620 may_prompt=False,
4621 verbose=options.verbose,
4622 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004623 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004624
4625
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004626def GenerateGerritChangeId(message):
4627 """Returns Ixxxxxx...xxx change id.
4628
4629 Works the same way as
4630 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4631 but can be called on demand on all platforms.
4632
4633 The basic idea is to generate git hash of a state of the tree, original commit
4634 message, author/committer info and timestamps.
4635 """
4636 lines = []
4637 tree_hash = RunGitSilent(['write-tree'])
4638 lines.append('tree %s' % tree_hash.strip())
4639 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4640 if code == 0:
4641 lines.append('parent %s' % parent.strip())
4642 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4643 lines.append('author %s' % author.strip())
4644 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4645 lines.append('committer %s' % committer.strip())
4646 lines.append('')
4647 # Note: Gerrit's commit-hook actually cleans message of some lines and
4648 # whitespace. This code is not doing this, but it clearly won't decrease
4649 # entropy.
4650 lines.append(message)
4651 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4652 stdin='\n'.join(lines))
4653 return 'I%s' % change_hash.strip()
4654
4655
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004656def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004657 """Computes the remote branch ref to use for the CL.
4658
4659 Args:
4660 remote (str): The git remote for the CL.
4661 remote_branch (str): The git remote branch for the CL.
4662 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004663 """
4664 if not (remote and remote_branch):
4665 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004666
wittman@chromium.org455dc922015-01-26 20:15:50 +00004667 if target_branch:
4668 # Cannonicalize branch references to the equivalent local full symbolic
4669 # refs, which are then translated into the remote full symbolic refs
4670 # below.
4671 if '/' not in target_branch:
4672 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4673 else:
4674 prefix_replacements = (
4675 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4676 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4677 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4678 )
4679 match = None
4680 for regex, replacement in prefix_replacements:
4681 match = re.search(regex, target_branch)
4682 if match:
4683 remote_branch = target_branch.replace(match.group(0), replacement)
4684 break
4685 if not match:
4686 # This is a branch path but not one we recognize; use as-is.
4687 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004688 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4689 # Handle the refs that need to land in different refs.
4690 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004691
wittman@chromium.org455dc922015-01-26 20:15:50 +00004692 # Create the true path to the remote branch.
4693 # Does the following translation:
4694 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4695 # * refs/remotes/origin/master -> refs/heads/master
4696 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4697 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4698 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4699 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4700 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4701 'refs/heads/')
4702 elif remote_branch.startswith('refs/remotes/branch-heads'):
4703 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004704
wittman@chromium.org455dc922015-01-26 20:15:50 +00004705 return remote_branch
4706
4707
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004708def cleanup_list(l):
4709 """Fixes a list so that comma separated items are put as individual items.
4710
4711 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4712 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4713 """
4714 items = sum((i.split(',') for i in l), [])
4715 stripped_items = (i.strip() for i in items)
4716 return sorted(filter(None, stripped_items))
4717
4718
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004719@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004720def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004721 """Uploads the current changelist to codereview.
4722
4723 Can skip dependency patchset uploads for a branch by running:
4724 git config branch.branch_name.skip-deps-uploads True
4725 To unset run:
4726 git config --unset branch.branch_name.skip-deps-uploads
4727 Can also set the above globally by using the --global flag.
4728 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004729 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4730 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004731 parser.add_option('--bypass-watchlists', action='store_true',
4732 dest='bypass_watchlists',
4733 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004734 parser.add_option('-f', action='store_true', dest='force',
4735 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004736 parser.add_option('--message', '-m', dest='message',
4737 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004738 parser.add_option('-b', '--bug',
4739 help='pre-populate the bug number(s) for this issue. '
4740 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004741 parser.add_option('--message-file', dest='message_file',
4742 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004743 parser.add_option('--title', '-t', dest='title',
4744 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004745 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004746 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004747 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004748 parser.add_option('--tbrs',
4749 action='append', default=[],
4750 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004751 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004752 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004753 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004754 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004755 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004756 parser.add_option('--emulate_svn_auto_props',
4757 '--emulate-svn-auto-props',
4758 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004759 dest="emulate_svn_auto_props",
4760 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004761 parser.add_option('-c', '--use-commit-queue', action='store_true',
4762 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004763 parser.add_option('--private', action='store_true',
4764 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004765 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004766 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004767 metavar='TARGET',
4768 help='Apply CL to remote ref TARGET. ' +
4769 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004770 parser.add_option('--squash', action='store_true',
4771 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004772 parser.add_option('--no-squash', action='store_true',
4773 help='Don\'t squash multiple commits into one ' +
4774 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004775 parser.add_option('--topic', default=None,
4776 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004777 parser.add_option('--email', default=None,
4778 help='email address to use to connect to Rietveld')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004779 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4780 const='TBR', help='add a set of OWNERS to TBR')
4781 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4782 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004783 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4784 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004785 help='Send the patchset to do a CQ dry run right after '
4786 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004787 parser.add_option('--dependencies', action='store_true',
4788 help='Uploads CLs of all the local branches that depend on '
4789 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004790
rmistry@google.com2dd99862015-06-22 12:22:18 +00004791 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004792 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004793 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004794 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004795 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004796 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004797 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004798
sbc@chromium.org71437c02015-04-09 19:29:40 +00004799 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004800 return 1
4801
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004802 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004803 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004804 options.cc = cleanup_list(options.cc)
4805
tandriib80458a2016-06-23 12:20:07 -07004806 if options.message_file:
4807 if options.message:
4808 parser.error('only one of --message and --message-file allowed.')
4809 options.message = gclient_utils.FileRead(options.message_file)
4810 options.message_file = None
4811
tandrii4d0545a2016-07-06 03:56:49 -07004812 if options.cq_dry_run and options.use_commit_queue:
4813 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4814
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004815 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4816 settings.GetIsGerrit()
4817
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004818 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004819 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004820
4821
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004822@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004823def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004824 """DEPRECATED: Used to commit the current changelist via git-svn."""
4825 message = ('git-cl no longer supports committing to SVN repositories via '
4826 'git-svn. You probably want to use `git cl land` instead.')
4827 print(message)
4828 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004829
4830
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004831# Two special branches used by git cl land.
4832MERGE_BRANCH = 'git-cl-commit'
4833CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4834
4835
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004836@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004837def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004838 """Commits the current changelist via git.
4839
4840 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4841 upstream and closes the issue automatically and atomically.
4842
4843 Otherwise (in case of Rietveld):
4844 Squashes branch into a single commit.
4845 Updates commit message with metadata (e.g. pointer to review).
4846 Pushes the code upstream.
4847 Updates review and closes.
4848 """
4849 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4850 help='bypass upload presubmit hook')
4851 parser.add_option('-m', dest='message',
4852 help="override review description")
4853 parser.add_option('-f', action='store_true', dest='force',
4854 help="force yes to questions (don't prompt)")
4855 parser.add_option('-c', dest='contributor',
4856 help="external contributor for patch (appended to " +
4857 "description and used as author for git). Should be " +
4858 "formatted as 'First Last <email@example.com>'")
4859 add_git_similarity(parser)
4860 auth.add_auth_options(parser)
4861 (options, args) = parser.parse_args(args)
4862 auth_config = auth.extract_auth_config_from_options(options)
4863
4864 cl = Changelist(auth_config=auth_config)
4865
4866 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4867 if cl.IsGerrit():
4868 if options.message:
4869 # This could be implemented, but it requires sending a new patch to
4870 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4871 # Besides, Gerrit has the ability to change the commit message on submit
4872 # automatically, thus there is no need to support this option (so far?).
4873 parser.error('-m MESSAGE option is not supported for Gerrit.')
4874 if options.contributor:
4875 parser.error(
4876 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4877 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4878 'the contributor\'s "name <email>". If you can\'t upload such a '
4879 'commit for review, contact your repository admin and request'
4880 '"Forge-Author" permission.')
4881 if not cl.GetIssue():
4882 DieWithError('You must upload the change first to Gerrit.\n'
4883 ' If you would rather have `git cl land` upload '
4884 'automatically for you, see http://crbug.com/642759')
4885 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4886 options.verbose)
4887
4888 current = cl.GetBranch()
4889 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4890 if remote == '.':
4891 print()
4892 print('Attempting to push branch %r into another local branch!' % current)
4893 print()
4894 print('Either reparent this branch on top of origin/master:')
4895 print(' git reparent-branch --root')
4896 print()
4897 print('OR run `git rebase-update` if you think the parent branch is ')
4898 print('already committed.')
4899 print()
4900 print(' Current parent: %r' % upstream_branch)
4901 return 1
4902
4903 if not args:
4904 # Default to merging against our best guess of the upstream branch.
4905 args = [cl.GetUpstreamBranch()]
4906
4907 if options.contributor:
4908 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4909 print("Please provide contibutor as 'First Last <email@example.com>'")
4910 return 1
4911
4912 base_branch = args[0]
4913
4914 if git_common.is_dirty_git_tree('land'):
4915 return 1
4916
4917 # This rev-list syntax means "show all commits not in my branch that
4918 # are in base_branch".
4919 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4920 base_branch]).splitlines()
4921 if upstream_commits:
4922 print('Base branch "%s" has %d commits '
4923 'not in this branch.' % (base_branch, len(upstream_commits)))
4924 print('Run "git merge %s" before attempting to land.' % base_branch)
4925 return 1
4926
4927 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4928 if not options.bypass_hooks:
4929 author = None
4930 if options.contributor:
4931 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4932 hook_results = cl.RunHook(
4933 committing=True,
4934 may_prompt=not options.force,
4935 verbose=options.verbose,
4936 change=cl.GetChange(merge_base, author))
4937 if not hook_results.should_continue():
4938 return 1
4939
4940 # Check the tree status if the tree status URL is set.
4941 status = GetTreeStatus()
4942 if 'closed' == status:
4943 print('The tree is closed. Please wait for it to reopen. Use '
4944 '"git cl land --bypass-hooks" to commit on a closed tree.')
4945 return 1
4946 elif 'unknown' == status:
4947 print('Unable to determine tree status. Please verify manually and '
4948 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4949 return 1
4950
4951 change_desc = ChangeDescription(options.message)
4952 if not change_desc.description and cl.GetIssue():
4953 change_desc = ChangeDescription(cl.GetDescription())
4954
4955 if not change_desc.description:
4956 if not cl.GetIssue() and options.bypass_hooks:
4957 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4958 else:
4959 print('No description set.')
4960 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4961 return 1
4962
4963 # Keep a separate copy for the commit message, because the commit message
4964 # contains the link to the Rietveld issue, while the Rietveld message contains
4965 # the commit viewvc url.
4966 if cl.GetIssue():
Aaron Gablea1bab272017-04-11 16:38:18 -07004967 change_desc.update_reviewers(
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004968 get_approving_reviewers(cl.GetIssueProperties()), [])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004969
4970 commit_desc = ChangeDescription(change_desc.description)
4971 if cl.GetIssue():
4972 # Xcode won't linkify this URL unless there is a non-whitespace character
4973 # after it. Add a period on a new line to circumvent this. Also add a space
4974 # before the period to make sure that Gitiles continues to correctly resolve
4975 # the URL.
4976 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4977 if options.contributor:
4978 commit_desc.append_footer('Patch from %s.' % options.contributor)
4979
4980 print('Description:')
4981 print(commit_desc.description)
4982
4983 branches = [merge_base, cl.GetBranchRef()]
4984 if not options.force:
4985 print_stats(options.similarity, options.find_copies, branches)
4986
4987 # We want to squash all this branch's commits into one commit with the proper
4988 # description. We do this by doing a "reset --soft" to the base branch (which
4989 # keeps the working copy the same), then landing that.
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004990 # Delete the special branches if they exist.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004991 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4992 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4993 result = RunGitWithCode(showref_cmd)
4994 if result[0] == 0:
4995 RunGit(['branch', '-D', branch])
4996
4997 # We might be in a directory that's present in this branch but not in the
4998 # trunk. Move up to the top of the tree so that git commands that expect a
4999 # valid CWD won't fail after we check out the merge branch.
5000 rel_base_path = settings.GetRelativeRoot()
5001 if rel_base_path:
5002 os.chdir(rel_base_path)
5003
5004 # Stuff our change into the merge branch.
5005 # We wrap in a try...finally block so if anything goes wrong,
5006 # we clean up the branches.
5007 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005008 revision = None
5009 try:
5010 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
5011 RunGit(['reset', '--soft', merge_base])
5012 if options.contributor:
5013 RunGit(
5014 [
5015 'commit', '--author', options.contributor,
5016 '-m', commit_desc.description,
5017 ])
5018 else:
5019 RunGit(['commit', '-m', commit_desc.description])
5020
5021 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
5022 mirror = settings.GetGitMirror(remote)
5023 if mirror:
5024 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005025 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005026 else:
5027 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005028 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005029 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
5030
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005031 retcode = PushToGitWithAutoRebase(
5032 pushurl, branch, commit_desc.description, git_numberer_enabled)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005033 if retcode == 0:
5034 revision = RunGit(['rev-parse', 'HEAD']).strip()
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005035 if git_numberer_enabled:
5036 change_desc = ChangeDescription(
5037 RunGit(['show', '-s', '--format=%B', 'HEAD']).strip())
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005038 except: # pylint: disable=bare-except
5039 if _IS_BEING_TESTED:
5040 logging.exception('this is likely your ACTUAL cause of test failure.\n'
5041 + '-' * 30 + '8<' + '-' * 30)
5042 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
5043 raise
5044 finally:
5045 # And then swap back to the original branch and clean up.
5046 RunGit(['checkout', '-q', cl.GetBranch()])
5047 RunGit(['branch', '-D', MERGE_BRANCH])
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005048 RunGit(['branch', '-D', CHERRY_PICK_BRANCH], error_ok=True)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005049
5050 if not revision:
5051 print('Failed to push. If this persists, please file a bug.')
5052 return 1
5053
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005054 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005055 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005056 if viewvc_url and revision:
5057 change_desc.append_footer(
5058 'Committed: %s%s' % (viewvc_url, revision))
5059 elif revision:
5060 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005061 print('Closing issue '
5062 '(you may be prompted for your codereview password)...')
5063 cl.UpdateDescription(change_desc.description)
5064 cl.CloseIssue()
5065 props = cl.GetIssueProperties()
5066 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005067 comment = "Committed patchset #%d (id:%d) manually as %s" % (
5068 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005069 if options.bypass_hooks:
5070 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
5071 else:
5072 comment += ' (presubmit successful).'
5073 cl.RpcServer().add_comment(cl.GetIssue(), comment)
5074
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005075 if os.path.isfile(POSTUPSTREAM_HOOK):
5076 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
5077
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005078 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005079
5080
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005081def PushToGitWithAutoRebase(remote, branch, original_description,
5082 git_numberer_enabled, max_attempts=3):
5083 """Pushes current HEAD commit on top of remote's branch.
5084
5085 Attempts to fetch and autorebase on push failures.
5086 Adds git number footers on the fly.
5087
5088 Returns integer code from last command.
5089 """
5090 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5091 code = 0
5092 attempts_left = max_attempts
5093 while attempts_left:
5094 attempts_left -= 1
5095 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5096
5097 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5098 # If fetch fails, retry.
5099 print('Fetching %s/%s...' % (remote, branch))
5100 code, out = RunGitWithCode(
5101 ['retry', 'fetch', remote,
5102 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5103 if code:
5104 print('Fetch failed with exit code %d.' % code)
5105 print(out.strip())
5106 continue
5107
5108 print('Cherry-picking commit on top of latest %s' % branch)
5109 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5110 suppress_stderr=True)
5111 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5112 code, out = RunGitWithCode(['cherry-pick', cherry])
5113 if code:
5114 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5115 'the following files have merge conflicts:' %
5116 (branch, parent_hash))
5117 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
5118 print('Please rebase your patch and try again.')
5119 RunGitWithCode(['cherry-pick', '--abort'])
5120 break
5121
5122 commit_desc = ChangeDescription(original_description)
5123 if git_numberer_enabled:
5124 logging.debug('Adding git number footers')
5125 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5126 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5127 branch)
5128 # Ensure timestamps are monotonically increasing.
5129 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5130 _get_committer_timestamp('HEAD'))
5131 _git_amend_head(commit_desc.description, timestamp)
5132
5133 code, out = RunGitWithCode(
5134 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5135 print(out)
5136 if code == 0:
5137 break
5138 if IsFatalPushFailure(out):
5139 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005140 'user.email are correct and you have push access to the repo.\n'
5141 'Hint: run command below to diangose common Git/Gerrit credential '
5142 'problems:\n'
5143 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005144 break
5145 return code
5146
5147
5148def IsFatalPushFailure(push_stdout):
5149 """True if retrying push won't help."""
5150 return '(prohibited by Gerrit)' in push_stdout
5151
5152
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005153@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005154def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005155 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005156 parser.add_option('-b', dest='newbranch',
5157 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005158 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005159 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005160 parser.add_option('-d', '--directory', action='store', metavar='DIR',
5161 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005162 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005163 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005164 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005165 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005166 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005167 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005168
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005169
5170 group = optparse.OptionGroup(
5171 parser,
5172 'Options for continuing work on the current issue uploaded from a '
5173 'different clone (e.g. different machine). Must be used independently '
5174 'from the other options. No issue number should be specified, and the '
5175 'branch must have an issue number associated with it')
5176 group.add_option('--reapply', action='store_true', dest='reapply',
5177 help='Reset the branch and reapply the issue.\n'
5178 'CAUTION: This will undo any local changes in this '
5179 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005180
5181 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005182 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005183 parser.add_option_group(group)
5184
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005185 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005186 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005187 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005188 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005189 auth_config = auth.extract_auth_config_from_options(options)
5190
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005191 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005192 if options.newbranch:
5193 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005194 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005195 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005196
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005197 cl = Changelist(auth_config=auth_config,
5198 codereview=options.forced_codereview)
5199 if not cl.GetIssue():
5200 parser.error('current branch must have an associated issue')
5201
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005202 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005203 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005204 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005205
5206 RunGit(['reset', '--hard', upstream])
5207 if options.pull:
5208 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005209
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005210 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5211 options.directory)
5212
5213 if len(args) != 1 or not args[0]:
5214 parser.error('Must specify issue number or url')
5215
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005216 target_issue_arg = ParseIssueNumberArgument(args[0],
5217 options.forced_codereview)
5218 if not target_issue_arg.valid:
5219 parser.error('invalid codereview url or CL id')
5220
5221 cl_kwargs = {
5222 'auth_config': auth_config,
5223 'codereview_host': target_issue_arg.hostname,
5224 'codereview': options.forced_codereview,
5225 }
5226 detected_codereview_from_url = False
5227 if target_issue_arg.codereview and not options.forced_codereview:
5228 detected_codereview_from_url = True
5229 cl_kwargs['codereview'] = target_issue_arg.codereview
5230 cl_kwargs['issue'] = target_issue_arg.issue
5231
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005232 # We don't want uncommitted changes mixed up with the patch.
5233 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005234 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005235
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005236 if options.newbranch:
5237 if options.force:
5238 RunGit(['branch', '-D', options.newbranch],
5239 stderr=subprocess2.PIPE, error_ok=True)
5240 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07005241 elif not GetCurrentBranch():
5242 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005243
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005244 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005245
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005246 if cl.IsGerrit():
5247 if options.reject:
5248 parser.error('--reject is not supported with Gerrit codereview.')
5249 if options.nocommit:
5250 parser.error('--nocommit is not supported with Gerrit codereview.')
5251 if options.directory:
5252 parser.error('--directory is not supported with Gerrit codereview.')
5253
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005254 if detected_codereview_from_url:
5255 print('canonical issue/change URL: %s (type: %s)\n' %
5256 (cl.GetIssueURL(), target_issue_arg.codereview))
5257
5258 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
5259 options.nocommit, options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005260
5261
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005262def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005263 """Fetches the tree status and returns either 'open', 'closed',
5264 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005265 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005266 if url:
5267 status = urllib2.urlopen(url).read().lower()
5268 if status.find('closed') != -1 or status == '0':
5269 return 'closed'
5270 elif status.find('open') != -1 or status == '1':
5271 return 'open'
5272 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005273 return 'unset'
5274
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005275
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005276def GetTreeStatusReason():
5277 """Fetches the tree status from a json url and returns the message
5278 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005279 url = settings.GetTreeStatusUrl()
5280 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005281 connection = urllib2.urlopen(json_url)
5282 status = json.loads(connection.read())
5283 connection.close()
5284 return status['message']
5285
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005286
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005287def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005288 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005289 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005290 status = GetTreeStatus()
5291 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005292 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005293 return 2
5294
vapiera7fbd5a2016-06-16 09:17:49 -07005295 print('The tree is %s' % status)
5296 print()
5297 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005298 if status != 'open':
5299 return 1
5300 return 0
5301
5302
maruel@chromium.org15192402012-09-06 12:38:29 +00005303def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005304 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005305 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005306 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005307 '-b', '--bot', action='append',
5308 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5309 'times to specify multiple builders. ex: '
5310 '"-b win_rel -b win_layout". See '
5311 'the try server waterfall for the builders name and the tests '
5312 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005313 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005314 '-B', '--bucket', default='',
5315 help=('Buildbucket bucket to send the try requests.'))
5316 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005317 '-m', '--master', default='',
5318 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005319 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005320 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005321 help='Revision to use for the try job; default: the revision will '
5322 'be determined by the try recipe that builder runs, which usually '
5323 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005324 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005325 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005326 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005327 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005328 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005329 '--project',
5330 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005331 'in recipe to determine to which repository or directory to '
5332 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005333 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005334 '-p', '--property', dest='properties', action='append', default=[],
5335 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005336 'key2=value2 etc. The value will be treated as '
5337 'json if decodable, or as string otherwise. '
5338 'NOTE: using this may make your try job not usable for CQ, '
5339 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005340 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005341 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5342 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005343 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005344 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005345 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005346 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005347
machenbach@chromium.org45453142015-09-15 08:45:22 +00005348 # Make sure that all properties are prop=value pairs.
5349 bad_params = [x for x in options.properties if '=' not in x]
5350 if bad_params:
5351 parser.error('Got properties with missing "=": %s' % bad_params)
5352
maruel@chromium.org15192402012-09-06 12:38:29 +00005353 if args:
5354 parser.error('Unknown arguments: %s' % args)
5355
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005356 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00005357 if not cl.GetIssue():
5358 parser.error('Need to upload first')
5359
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005360 if cl.IsGerrit():
5361 # HACK: warm up Gerrit change detail cache to save on RPCs.
5362 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5363
tandriie113dfd2016-10-11 10:20:12 -07005364 error_message = cl.CannotTriggerTryJobReason()
5365 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005366 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005367
borenet6c0efe62016-10-19 08:13:29 -07005368 if options.bucket and options.master:
5369 parser.error('Only one of --bucket and --master may be used.')
5370
qyearsley1fdfcb62016-10-24 13:22:03 -07005371 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005372
qyearsleydd49f942016-10-28 11:57:22 -07005373 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5374 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005375 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005376 if options.verbose:
5377 print('git cl try with no bots now defaults to CQ Dry Run.')
5378 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00005379
borenet6c0efe62016-10-19 08:13:29 -07005380 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005381 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005382 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005383 'of bot requires an initial job from a parent (usually a builder). '
5384 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005385 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005386 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005387
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005388 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005389 # TODO(tandrii): Checking local patchset against remote patchset is only
5390 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5391 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005392 print('Warning: Codereview server has newer patchsets (%s) than most '
5393 'recent upload from local checkout (%s). Did a previous upload '
5394 'fail?\n'
5395 'By default, git cl try uses the latest patchset from '
5396 'codereview, continuing to use patchset %s.\n' %
5397 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005398
tandrii568043b2016-10-11 07:49:18 -07005399 try:
borenet6c0efe62016-10-19 08:13:29 -07005400 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5401 patchset)
tandrii568043b2016-10-11 07:49:18 -07005402 except BuildbucketResponseException as ex:
5403 print('ERROR: %s' % ex)
5404 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005405 return 0
5406
5407
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005408def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005409 """Prints info about try jobs associated with current CL."""
5410 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005411 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005412 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005413 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005414 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005415 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005416 '--color', action='store_true', default=setup_color.IS_TTY,
5417 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005418 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005419 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5420 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005421 group.add_option(
5422 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005423 parser.add_option_group(group)
5424 auth.add_auth_options(parser)
5425 options, args = parser.parse_args(args)
5426 if args:
5427 parser.error('Unrecognized args: %s' % ' '.join(args))
5428
5429 auth_config = auth.extract_auth_config_from_options(options)
5430 cl = Changelist(auth_config=auth_config)
5431 if not cl.GetIssue():
5432 parser.error('Need to upload first')
5433
tandrii221ab252016-10-06 08:12:04 -07005434 patchset = options.patchset
5435 if not patchset:
5436 patchset = cl.GetMostRecentPatchset()
5437 if not patchset:
5438 parser.error('Codereview doesn\'t know about issue %s. '
5439 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005440 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005441 cl.GetIssue())
5442
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005443 # TODO(tandrii): Checking local patchset against remote patchset is only
5444 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5445 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005446 print('Warning: Codereview server has newer patchsets (%s) than most '
5447 'recent upload from local checkout (%s). Did a previous upload '
5448 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005449 'By default, git cl try-results uses the latest patchset from '
5450 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005451 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005452 try:
tandrii221ab252016-10-06 08:12:04 -07005453 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005454 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005455 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005456 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005457 if options.json:
5458 write_try_results_json(options.json, jobs)
5459 else:
5460 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005461 return 0
5462
5463
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005464@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005465def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005466 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005467 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005468 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005469 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005470
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005471 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005472 if args:
5473 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005474 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005475 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005476 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005477 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005478
5479 # Clear configured merge-base, if there is one.
5480 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005481 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005482 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005483 return 0
5484
5485
thestig@chromium.org00858c82013-12-02 23:08:03 +00005486def CMDweb(parser, args):
5487 """Opens the current CL in the web browser."""
5488 _, args = parser.parse_args(args)
5489 if args:
5490 parser.error('Unrecognized args: %s' % ' '.join(args))
5491
5492 issue_url = Changelist().GetIssueURL()
5493 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005494 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005495 return 1
5496
5497 webbrowser.open(issue_url)
5498 return 0
5499
5500
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005501def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005502 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005503 parser.add_option('-d', '--dry-run', action='store_true',
5504 help='trigger in dry run mode')
5505 parser.add_option('-c', '--clear', action='store_true',
5506 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005507 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005508 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005509 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005510 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005511 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005512 if args:
5513 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005514 if options.dry_run and options.clear:
5515 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5516
iannuccie53c9352016-08-17 14:40:40 -07005517 cl = Changelist(auth_config=auth_config, issue=options.issue,
5518 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005519 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005520 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005521 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005522 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005523 state = _CQState.DRY_RUN
5524 else:
5525 state = _CQState.COMMIT
5526 if not cl.GetIssue():
5527 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005528 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005529 return 0
5530
5531
groby@chromium.org411034a2013-02-26 15:12:01 +00005532def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005533 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005534 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005535 auth.add_auth_options(parser)
5536 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005537 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005538 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005539 if args:
5540 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005541 cl = Changelist(auth_config=auth_config, issue=options.issue,
5542 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005543 # Ensure there actually is an issue to close.
5544 cl.GetDescription()
5545 cl.CloseIssue()
5546 return 0
5547
5548
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005549def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005550 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005551 parser.add_option(
5552 '--stat',
5553 action='store_true',
5554 dest='stat',
5555 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005556 auth.add_auth_options(parser)
5557 options, args = parser.parse_args(args)
5558 auth_config = auth.extract_auth_config_from_options(options)
5559 if args:
5560 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005561
5562 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005563 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005564 # Staged changes would be committed along with the patch from last
5565 # upload, hence counted toward the "last upload" side in the final
5566 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005567 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005568 return 1
5569
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005570 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005571 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005572 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005573 if not issue:
5574 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005575 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005576 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005577
5578 # Create a new branch based on the merge-base
5579 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005580 # Clear cached branch in cl object, to avoid overwriting original CL branch
5581 # properties.
5582 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005583 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005584 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005585 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005586 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005587 return rtn
5588
wychen@chromium.org06928532015-02-03 02:11:29 +00005589 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005590 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005591 cmd = ['git', 'diff']
5592 if options.stat:
5593 cmd.append('--stat')
5594 cmd.extend([TMP_BRANCH, branch, '--'])
5595 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005596 finally:
5597 RunGit(['checkout', '-q', branch])
5598 RunGit(['branch', '-D', TMP_BRANCH])
5599
5600 return 0
5601
5602
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005603def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005604 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005605 parser.add_option(
5606 '--no-color',
5607 action='store_true',
5608 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005609 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005610 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005611 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005612
5613 author = RunGit(['config', 'user.email']).strip() or None
5614
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005615 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005616
5617 if args:
5618 if len(args) > 1:
5619 parser.error('Unknown args')
5620 base_branch = args[0]
5621 else:
5622 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005623 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005624
5625 change = cl.GetChange(base_branch, None)
5626 return owners_finder.OwnersFinder(
5627 [f.LocalPath() for f in
5628 cl.GetChange(base_branch, None).AffectedFiles()],
Jochen Eisinger72606f82017-04-04 10:44:18 +02005629 change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02005630 author, fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005631 disable_color=options.no_color,
5632 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005633
5634
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005635def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005636 """Generates a diff command."""
5637 # Generate diff for the current branch's changes.
5638 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005639 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005640
5641 if args:
5642 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005643 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005644 diff_cmd.append(arg)
5645 else:
5646 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005647
5648 return diff_cmd
5649
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005650
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005651def MatchingFileType(file_name, extensions):
5652 """Returns true if the file name ends with one of the given extensions."""
5653 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005654
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005655
enne@chromium.org555cfe42014-01-29 18:21:39 +00005656@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005657def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005658 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005659 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005660 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005661 parser.add_option('--full', action='store_true',
5662 help='Reformat the full content of all touched files')
5663 parser.add_option('--dry-run', action='store_true',
5664 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005665 parser.add_option('--python', action='store_true',
5666 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005667 parser.add_option('--js', action='store_true',
5668 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005669 parser.add_option('--diff', action='store_true',
5670 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005671 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005672
Daniel Chengc55eecf2016-12-30 03:11:02 -08005673 # Normalize any remaining args against the current path, so paths relative to
5674 # the current directory are still resolved as expected.
5675 args = [os.path.join(os.getcwd(), arg) for arg in args]
5676
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005677 # git diff generates paths against the root of the repository. Change
5678 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005679 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005680 if rel_base_path:
5681 os.chdir(rel_base_path)
5682
digit@chromium.org29e47272013-05-17 17:01:46 +00005683 # Grab the merge-base commit, i.e. the upstream commit of the current
5684 # branch when it was created or the last time it was rebased. This is
5685 # to cover the case where the user may have called "git fetch origin",
5686 # moving the origin branch to a newer commit, but hasn't rebased yet.
5687 upstream_commit = None
5688 cl = Changelist()
5689 upstream_branch = cl.GetUpstreamBranch()
5690 if upstream_branch:
5691 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5692 upstream_commit = upstream_commit.strip()
5693
5694 if not upstream_commit:
5695 DieWithError('Could not find base commit for this branch. '
5696 'Are you in detached state?')
5697
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005698 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5699 diff_output = RunGit(changed_files_cmd)
5700 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005701 # Filter out files deleted by this CL
5702 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005703
Christopher Lamc5ba6922017-01-24 11:19:14 +11005704 if opts.js:
5705 CLANG_EXTS.append('.js')
5706
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005707 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5708 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5709 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005710 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005711
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005712 top_dir = os.path.normpath(
5713 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5714
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005715 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5716 # formatted. This is used to block during the presubmit.
5717 return_value = 0
5718
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005719 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005720 # Locate the clang-format binary in the checkout
5721 try:
5722 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005723 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005724 DieWithError(e)
5725
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005726 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005727 cmd = [clang_format_tool]
5728 if not opts.dry_run and not opts.diff:
5729 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005730 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005731 if opts.diff:
5732 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005733 else:
5734 env = os.environ.copy()
5735 env['PATH'] = str(os.path.dirname(clang_format_tool))
5736 try:
5737 script = clang_format.FindClangFormatScriptInChromiumTree(
5738 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005739 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005740 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005741
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005742 cmd = [sys.executable, script, '-p0']
5743 if not opts.dry_run and not opts.diff:
5744 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005745
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005746 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5747 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005748
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005749 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5750 if opts.diff:
5751 sys.stdout.write(stdout)
5752 if opts.dry_run and len(stdout) > 0:
5753 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005754
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005755 # Similar code to above, but using yapf on .py files rather than clang-format
5756 # on C/C++ files
5757 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005758 yapf_tool = gclient_utils.FindExecutable('yapf')
5759 if yapf_tool is None:
5760 DieWithError('yapf not found in PATH')
5761
5762 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005763 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005764 cmd = [yapf_tool]
5765 if not opts.dry_run and not opts.diff:
5766 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005767 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005768 if opts.diff:
5769 sys.stdout.write(stdout)
5770 else:
5771 # TODO(sbc): yapf --lines mode still has some issues.
5772 # https://github.com/google/yapf/issues/154
5773 DieWithError('--python currently only works with --full')
5774
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005775 # Dart's formatter does not have the nice property of only operating on
5776 # modified chunks, so hard code full.
5777 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005778 try:
5779 command = [dart_format.FindDartFmtToolInChromiumTree()]
5780 if not opts.dry_run and not opts.diff:
5781 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005782 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005783
ppi@chromium.org6593d932016-03-03 15:41:15 +00005784 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005785 if opts.dry_run and stdout:
5786 return_value = 2
5787 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005788 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5789 'found in this checkout. Files in other languages are still '
5790 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005791
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005792 # Format GN build files. Always run on full build files for canonical form.
5793 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005794 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005795 if opts.dry_run or opts.diff:
5796 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005797 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005798 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5799 shell=sys.platform == 'win32',
5800 cwd=top_dir)
5801 if opts.dry_run and gn_ret == 2:
5802 return_value = 2 # Not formatted.
5803 elif opts.diff and gn_ret == 2:
5804 # TODO this should compute and print the actual diff.
5805 print("This change has GN build file diff for " + gn_diff_file)
5806 elif gn_ret != 0:
5807 # For non-dry run cases (and non-2 return values for dry-run), a
5808 # nonzero error code indicates a failure, probably because the file
5809 # doesn't parse.
5810 DieWithError("gn format failed on " + gn_diff_file +
5811 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005812
Steven Holte2e664bf2017-04-21 13:10:47 -07005813 for xml_dir in GetDirtyMetricsDirs(diff_files):
5814 tool_dir = os.path.join(top_dir, xml_dir)
5815 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5816 if opts.dry_run or opts.diff:
5817 cmd.append('--diff')
5818 stdout = RunCommand(cmd, cwd=top_dir)
5819 if opts.diff:
5820 sys.stdout.write(stdout)
5821 if opts.dry_run and stdout:
5822 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005823
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005824 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005825
Steven Holte2e664bf2017-04-21 13:10:47 -07005826def GetDirtyMetricsDirs(diff_files):
5827 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5828 metrics_xml_dirs = [
5829 os.path.join('tools', 'metrics', 'actions'),
5830 os.path.join('tools', 'metrics', 'histograms'),
5831 os.path.join('tools', 'metrics', 'rappor'),
5832 os.path.join('tools', 'metrics', 'ukm')]
5833 for xml_dir in metrics_xml_dirs:
5834 if any(file.startswith(xml_dir) for file in xml_diff_files):
5835 yield xml_dir
5836
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005837
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005838@subcommand.usage('<codereview url or issue id>')
5839def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005840 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005841 _, args = parser.parse_args(args)
5842
5843 if len(args) != 1:
5844 parser.print_help()
5845 return 1
5846
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005847 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005848 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005849 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005850
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005851 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005852
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005853 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005854 output = RunGit(['config', '--local', '--get-regexp',
5855 r'branch\..*\.%s' % issueprefix],
5856 error_ok=True)
5857 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005858 if issue == target_issue:
5859 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005860
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005861 branches = []
5862 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005863 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005864 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005865 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005866 return 1
5867 if len(branches) == 1:
5868 RunGit(['checkout', branches[0]])
5869 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005870 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005871 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005872 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005873 which = raw_input('Choose by index: ')
5874 try:
5875 RunGit(['checkout', branches[int(which)]])
5876 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005877 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005878 return 1
5879
5880 return 0
5881
5882
maruel@chromium.org29404b52014-09-08 22:58:00 +00005883def CMDlol(parser, args):
5884 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005885 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005886 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5887 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5888 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005889 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005890 return 0
5891
5892
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005893class OptionParser(optparse.OptionParser):
5894 """Creates the option parse and add --verbose support."""
5895 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005896 optparse.OptionParser.__init__(
5897 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005898 self.add_option(
5899 '-v', '--verbose', action='count', default=0,
5900 help='Use 2 times for more debugging info')
5901
5902 def parse_args(self, args=None, values=None):
5903 options, args = optparse.OptionParser.parse_args(self, args, values)
5904 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005905 logging.basicConfig(
5906 level=levels[min(options.verbose, len(levels) - 1)],
5907 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5908 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005909 return options, args
5910
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005911
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005912def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005913 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005914 print('\nYour python version %s is unsupported, please upgrade.\n' %
5915 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005916 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005917
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005918 # Reload settings.
5919 global settings
5920 settings = Settings()
5921
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005922 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005923 dispatcher = subcommand.CommandDispatcher(__name__)
5924 try:
5925 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005926 except auth.AuthenticationError as e:
5927 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005928 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005929 if e.code != 500:
5930 raise
5931 DieWithError(
5932 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5933 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005934 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005935
5936
5937if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005938 # These affect sys.stdout so do it outside of main() to simplify mocks in
5939 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005940 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005941 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005942 try:
5943 sys.exit(main(sys.argv[1:]))
5944 except KeyboardInterrupt:
5945 sys.stderr.write('interrupted\n')
5946 sys.exit(1)