blob: 3f1752f01de10abb11dbe75a8d93fa2f9aa2c101 [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
327 print('Using %d%% similarity for rename/copy detection. '
328 'Override with --similarity.' % options.similarity)
329
330 return options, args
331 parser.parse_args = Parse
332
333
machenbach@chromium.org45453142015-09-15 08:45:22 +0000334def _get_properties_from_options(options):
335 properties = dict(x.split('=', 1) for x in options.properties)
336 for key, val in properties.iteritems():
337 try:
338 properties[key] = json.loads(val)
339 except ValueError:
340 pass # If a value couldn't be evaluated, treat it as a string.
341 return properties
342
343
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000344def _prefix_master(master):
345 """Convert user-specified master name to full master name.
346
347 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
348 name, while the developers always use shortened master name
349 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
350 function does the conversion for buildbucket migration.
351 """
borenet6c0efe62016-10-19 08:13:29 -0700352 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000353 return master
borenet6c0efe62016-10-19 08:13:29 -0700354 return '%s%s' % (MASTER_PREFIX, master)
355
356
357def _unprefix_master(bucket):
358 """Convert bucket name to shortened master name.
359
360 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
361 name, while the developers always use shortened master name
362 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
363 function does the conversion for buildbucket migration.
364 """
365 if bucket.startswith(MASTER_PREFIX):
366 return bucket[len(MASTER_PREFIX):]
367 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368
369
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000370def _buildbucket_retry(operation_name, http, *args, **kwargs):
371 """Retries requests to buildbucket service and returns parsed json content."""
372 try_count = 0
373 while True:
374 response, content = http.request(*args, **kwargs)
375 try:
376 content_json = json.loads(content)
377 except ValueError:
378 content_json = None
379
380 # Buildbucket could return an error even if status==200.
381 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000382 error = content_json.get('error')
383 if error.get('code') == 403:
384 raise BuildbucketResponseException(
385 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000386 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000387 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000388 raise BuildbucketResponseException(msg)
389
390 if response.status == 200:
391 if not content_json:
392 raise BuildbucketResponseException(
393 'Buildbucket returns invalid json content: %s.\n'
394 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
395 content)
396 return content_json
397 if response.status < 500 or try_count >= 2:
398 raise httplib2.HttpLib2Error(content)
399
400 # status >= 500 means transient failures.
401 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700402 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000403 try_count += 1
404 assert False, 'unreachable'
405
406
qyearsley1fdfcb62016-10-24 13:22:03 -0700407def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700408 """Returns a dict mapping bucket names to builders and tests,
409 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 """
qyearsleydd49f942016-10-28 11:57:22 -0700411 # If no bots are listed, we try to get a set of builders and tests based
412 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700413 if not options.bot:
414 change = changelist.GetChange(
415 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700416 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700417 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700418 change=change,
419 changed_files=change.LocalPaths(),
420 repository_root=settings.GetRoot(),
421 default_presubmit=None,
422 project=None,
423 verbose=options.verbose,
424 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700425 if masters is None:
426 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100427 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700428
qyearsley1fdfcb62016-10-24 13:22:03 -0700429 if options.bucket:
430 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700431 if options.master:
432 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700433
qyearsleydd49f942016-10-28 11:57:22 -0700434 # If bots are listed but no master or bucket, then we need to find out
435 # the corresponding master for each bot.
436 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
437 if error_message:
438 option_parser.error(
439 'Tryserver master cannot be found because: %s\n'
440 'Please manually specify the tryserver master, e.g. '
441 '"-m tryserver.chromium.linux".' % error_message)
442 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700443
444
qyearsley123a4682016-10-26 09:12:17 -0700445def _get_bucket_map_for_builders(builders):
446 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700447 map_url = 'https://builders-map.appspot.com/'
448 try:
qyearsley123a4682016-10-26 09:12:17 -0700449 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700450 except urllib2.URLError as e:
451 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
452 (map_url, e))
453 except ValueError as e:
454 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700455 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700456 return None, 'Failed to build master map.'
457
qyearsley123a4682016-10-26 09:12:17 -0700458 bucket_map = {}
459 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700460 masters = builders_map.get(builder, [])
461 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700462 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700463 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700464 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700465 (builder, masters))
466 bucket = _prefix_master(masters[0])
467 bucket_map.setdefault(bucket, {})[builder] = []
468
469 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700470
471
borenet6c0efe62016-10-19 08:13:29 -0700472def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700473 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700474 """Sends a request to Buildbucket to trigger try jobs for a changelist.
475
476 Args:
477 auth_config: AuthConfig for Rietveld.
478 changelist: Changelist that the try jobs are associated with.
479 buckets: A nested dict mapping bucket names to builders to tests.
480 options: Command-line options.
481 """
tandriide281ae2016-10-12 06:02:30 -0700482 assert changelist.GetIssue(), 'CL must be uploaded first'
483 codereview_url = changelist.GetCodereviewServer()
484 assert codereview_url, 'CL must be uploaded first'
485 patchset = patchset or changelist.GetMostRecentPatchset()
486 assert patchset, 'CL must be uploaded first'
487
488 codereview_host = urlparse.urlparse(codereview_url).hostname
489 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000490 http = authenticator.authorize(httplib2.Http())
491 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700492
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000493 buildbucket_put_url = (
494 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000495 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700496 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
497 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
498 hostname=codereview_host,
499 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000500 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700501
502 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
503 shared_parameters_properties['category'] = category
504 if options.clobber:
505 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700506 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700507 if extra_properties:
508 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000509
510 batch_req_body = {'builds': []}
511 print_text = []
512 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700513 for bucket, builders_and_tests in sorted(buckets.iteritems()):
514 print_text.append('Bucket: %s' % bucket)
515 master = None
516 if bucket.startswith(MASTER_PREFIX):
517 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000518 for builder, tests in sorted(builders_and_tests.iteritems()):
519 print_text.append(' %s: %s' % (builder, tests))
520 parameters = {
521 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000522 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100523 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000524 'revision': options.revision,
525 }],
tandrii8c5a3532016-11-04 07:52:02 -0700526 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000527 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000528 if 'presubmit' in builder.lower():
529 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000530 if tests:
531 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700532
533 tags = [
534 'builder:%s' % builder,
535 'buildset:%s' % buildset,
536 'user_agent:git_cl_try',
537 ]
538 if master:
539 parameters['properties']['master'] = master
540 tags.append('master:%s' % master)
541
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000542 batch_req_body['builds'].append(
543 {
544 'bucket': bucket,
545 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700547 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000548 }
549 )
550
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700552 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 http,
554 buildbucket_put_url,
555 'PUT',
556 body=json.dumps(batch_req_body),
557 headers={'Content-Type': 'application/json'}
558 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000559 print_text.append('To see results here, run: git cl try-results')
560 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700561 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000562
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000563
tandrii221ab252016-10-06 08:12:04 -0700564def fetch_try_jobs(auth_config, changelist, buildbucket_host,
565 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700566 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567
qyearsley53f48a12016-09-01 10:45:13 -0700568 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 """
tandrii221ab252016-10-06 08:12:04 -0700570 assert buildbucket_host
571 assert changelist.GetIssue(), 'CL must be uploaded first'
572 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
573 patchset = patchset or changelist.GetMostRecentPatchset()
574 assert patchset, 'CL must be uploaded first'
575
576 codereview_url = changelist.GetCodereviewServer()
577 codereview_host = urlparse.urlparse(codereview_url).hostname
578 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 if authenticator.has_cached_credentials():
580 http = authenticator.authorize(httplib2.Http())
581 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700582 print('Warning: Some results might be missing because %s' %
583 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700584 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 http = httplib2.Http()
586
587 http.force_exception_to_status_code = True
588
tandrii221ab252016-10-06 08:12:04 -0700589 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
590 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
591 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000592 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700593 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000594 params = {'tag': 'buildset:%s' % buildset}
595
596 builds = {}
597 while True:
598 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700599 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000600 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700601 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000602 for build in content.get('builds', []):
603 builds[build['id']] = build
604 if 'next_cursor' in content:
605 params['start_cursor'] = content['next_cursor']
606 else:
607 break
608 return builds
609
610
qyearsleyeab3c042016-08-24 09:18:28 -0700611def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000612 """Prints nicely result of fetch_try_jobs."""
613 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700614 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000615 return
616
617 # Make a copy, because we'll be modifying builds dictionary.
618 builds = builds.copy()
619 builder_names_cache = {}
620
621 def get_builder(b):
622 try:
623 return builder_names_cache[b['id']]
624 except KeyError:
625 try:
626 parameters = json.loads(b['parameters_json'])
627 name = parameters['builder_name']
628 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700629 print('WARNING: failed to get builder name for build %s: %s' % (
630 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000631 name = None
632 builder_names_cache[b['id']] = name
633 return name
634
635 def get_bucket(b):
636 bucket = b['bucket']
637 if bucket.startswith('master.'):
638 return bucket[len('master.'):]
639 return bucket
640
641 if options.print_master:
642 name_fmt = '%%-%ds %%-%ds' % (
643 max(len(str(get_bucket(b))) for b in builds.itervalues()),
644 max(len(str(get_builder(b))) for b in builds.itervalues()))
645 def get_name(b):
646 return name_fmt % (get_bucket(b), get_builder(b))
647 else:
648 name_fmt = '%%-%ds' % (
649 max(len(str(get_builder(b))) for b in builds.itervalues()))
650 def get_name(b):
651 return name_fmt % get_builder(b)
652
653 def sort_key(b):
654 return b['status'], b.get('result'), get_name(b), b.get('url')
655
656 def pop(title, f, color=None, **kwargs):
657 """Pop matching builds from `builds` dict and print them."""
658
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000659 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000660 colorize = str
661 else:
662 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
663
664 result = []
665 for b in builds.values():
666 if all(b.get(k) == v for k, v in kwargs.iteritems()):
667 builds.pop(b['id'])
668 result.append(b)
669 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700670 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000671 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700672 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000673
674 total = len(builds)
675 pop(status='COMPLETED', result='SUCCESS',
676 title='Successes:', color=Fore.GREEN,
677 f=lambda b: (get_name(b), b.get('url')))
678 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
679 title='Infra Failures:', color=Fore.MAGENTA,
680 f=lambda b: (get_name(b), b.get('url')))
681 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
682 title='Failures:', color=Fore.RED,
683 f=lambda b: (get_name(b), b.get('url')))
684 pop(status='COMPLETED', result='CANCELED',
685 title='Canceled:', color=Fore.MAGENTA,
686 f=lambda b: (get_name(b),))
687 pop(status='COMPLETED', result='FAILURE',
688 failure_reason='INVALID_BUILD_DEFINITION',
689 title='Wrong master/builder name:', color=Fore.MAGENTA,
690 f=lambda b: (get_name(b),))
691 pop(status='COMPLETED', result='FAILURE',
692 title='Other failures:',
693 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
694 pop(status='COMPLETED',
695 title='Other finished:',
696 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
697 pop(status='STARTED',
698 title='Started:', color=Fore.YELLOW,
699 f=lambda b: (get_name(b), b.get('url')))
700 pop(status='SCHEDULED',
701 title='Scheduled:',
702 f=lambda b: (get_name(b), 'id=%s' % b['id']))
703 # The last section is just in case buildbucket API changes OR there is a bug.
704 pop(title='Other:',
705 f=lambda b: (get_name(b), 'id=%s' % b['id']))
706 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700707 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000708
709
qyearsley53f48a12016-09-01 10:45:13 -0700710def write_try_results_json(output_file, builds):
711 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
712
713 The input |builds| dict is assumed to be generated by Buildbucket.
714 Buildbucket documentation: http://goo.gl/G0s101
715 """
716
717 def convert_build_dict(build):
718 return {
719 'buildbucket_id': build.get('id'),
720 'status': build.get('status'),
721 'result': build.get('result'),
722 'bucket': build.get('bucket'),
723 'builder_name': json.loads(
724 build.get('parameters_json', '{}')).get('builder_name'),
725 'failure_reason': build.get('failure_reason'),
726 'url': build.get('url'),
727 }
728
729 converted = []
730 for _, build in sorted(builds.items()):
731 converted.append(convert_build_dict(build))
732 write_json(output_file, converted)
733
734
iannucci@chromium.org79540052012-10-19 23:15:26 +0000735def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000736 """Prints statistics about the change to the user."""
737 # --no-ext-diff is broken in some versions of Git, so try to work around
738 # this by overriding the environment (but there is still a problem if the
739 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000740 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000741 if 'GIT_EXTERNAL_DIFF' in env:
742 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000743
744 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800745 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000746 else:
747 similarity_options = ['-M%s' % similarity]
748
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000749 try:
750 stdout = sys.stdout.fileno()
751 except AttributeError:
752 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000753 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000754 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000755 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000756 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000757
758
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000759class BuildbucketResponseException(Exception):
760 pass
761
762
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000763class Settings(object):
764 def __init__(self):
765 self.default_server = None
766 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000767 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768 self.tree_status_url = None
769 self.viewvc_url = None
770 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000771 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000772 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000773 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000774 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000775 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000776 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777
778 def LazyUpdateIfNeeded(self):
779 """Updates the settings from a codereview.settings file, if available."""
780 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000781 # The only value that actually changes the behavior is
782 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000783 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000784 error_ok=True
785 ).strip().lower()
786
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000788 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789 LoadCodereviewSettingsFromFile(cr_settings_file)
790 self.updated = True
791
792 def GetDefaultServerUrl(self, error_ok=False):
793 if not self.default_server:
794 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000795 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000796 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 if error_ok:
798 return self.default_server
799 if not self.default_server:
800 error_message = ('Could not find settings file. You must configure '
801 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000802 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000803 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000804 return self.default_server
805
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000806 @staticmethod
807 def GetRelativeRoot():
808 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000811 if self.root is None:
812 self.root = os.path.abspath(self.GetRelativeRoot())
813 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000815 def GetGitMirror(self, remote='origin'):
816 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000817 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000818 if not os.path.isdir(local_url):
819 return None
820 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
821 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100822 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100823 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000824 if mirror.exists():
825 return mirror
826 return None
827
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828 def GetTreeStatusUrl(self, error_ok=False):
829 if not self.tree_status_url:
830 error_message = ('You must configure your tree status URL by running '
831 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000832 self.tree_status_url = self._GetRietveldConfig(
833 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 return self.tree_status_url
835
836 def GetViewVCUrl(self):
837 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000838 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839 return self.viewvc_url
840
rmistry@google.com90752582014-01-14 21:04:50 +0000841 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000842 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000843
rmistry@google.com78948ed2015-07-08 23:09:57 +0000844 def GetIsSkipDependencyUpload(self, branch_name):
845 """Returns true if specified branch should skip dep uploads."""
846 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
847 error_ok=True)
848
rmistry@google.com5626a922015-02-26 14:03:30 +0000849 def GetRunPostUploadHook(self):
850 run_post_upload_hook = self._GetRietveldConfig(
851 'run-post-upload-hook', error_ok=True)
852 return run_post_upload_hook == "True"
853
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000854 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000855 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000856
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000857 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000858 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000859
ukai@chromium.orge8077812012-02-03 03:41:46 +0000860 def GetIsGerrit(self):
861 """Return true if this repo is assosiated with gerrit code review system."""
862 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700863 self.is_gerrit = (
864 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000865 return self.is_gerrit
866
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000867 def GetSquashGerritUploads(self):
868 """Return true if uploads to Gerrit should be squashed by default."""
869 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700870 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
871 if self.squash_gerrit_uploads is None:
872 # Default is squash now (http://crbug.com/611892#c23).
873 self.squash_gerrit_uploads = not (
874 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
875 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000876 return self.squash_gerrit_uploads
877
tandriia60502f2016-06-20 02:01:53 -0700878 def GetSquashGerritUploadsOverride(self):
879 """Return True or False if codereview.settings should be overridden.
880
881 Returns None if no override has been defined.
882 """
883 # See also http://crbug.com/611892#c23
884 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
885 error_ok=True).strip()
886 if result == 'true':
887 return True
888 if result == 'false':
889 return False
890 return None
891
tandrii@chromium.org28253532016-04-14 13:46:56 +0000892 def GetGerritSkipEnsureAuthenticated(self):
893 """Return True if EnsureAuthenticated should not be done for Gerrit
894 uploads."""
895 if self.gerrit_skip_ensure_authenticated is None:
896 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000897 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000898 error_ok=True).strip() == 'true')
899 return self.gerrit_skip_ensure_authenticated
900
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000901 def GetGitEditor(self):
902 """Return the editor specified in the git config, or None if none is."""
903 if self.git_editor is None:
904 self.git_editor = self._GetConfig('core.editor', error_ok=True)
905 return self.git_editor or None
906
thestig@chromium.org44202a22014-03-11 19:22:18 +0000907 def GetLintRegex(self):
908 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
909 DEFAULT_LINT_REGEX)
910
911 def GetLintIgnoreRegex(self):
912 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
913 DEFAULT_LINT_IGNORE_REGEX)
914
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000915 def GetProject(self):
916 if not self.project:
917 self.project = self._GetRietveldConfig('project', error_ok=True)
918 return self.project
919
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000920 def _GetRietveldConfig(self, param, **kwargs):
921 return self._GetConfig('rietveld.' + param, **kwargs)
922
rmistry@google.com78948ed2015-07-08 23:09:57 +0000923 def _GetBranchConfig(self, branch_name, param, **kwargs):
924 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 def _GetConfig(self, param, **kwargs):
927 self.LazyUpdateIfNeeded()
928 return RunGit(['config', param], **kwargs).strip()
929
930
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100931@contextlib.contextmanager
932def _get_gerrit_project_config_file(remote_url):
933 """Context manager to fetch and store Gerrit's project.config from
934 refs/meta/config branch and store it in temp file.
935
936 Provides a temporary filename or None if there was error.
937 """
938 error, _ = RunGitWithCode([
939 'fetch', remote_url,
940 '+refs/meta/config:refs/git_cl/meta/config'])
941 if error:
942 # Ref doesn't exist or isn't accessible to current user.
943 print('WARNING: failed to fetch project config for %s: %s' %
944 (remote_url, error))
945 yield None
946 return
947
948 error, project_config_data = RunGitWithCode(
949 ['show', 'refs/git_cl/meta/config:project.config'])
950 if error:
951 print('WARNING: project.config file not found')
952 yield None
953 return
954
955 with gclient_utils.temporary_directory() as tempdir:
956 project_config_file = os.path.join(tempdir, 'project.config')
957 gclient_utils.FileWrite(project_config_file, project_config_data)
958 yield project_config_file
959
960
961def _is_git_numberer_enabled(remote_url, remote_ref):
962 """Returns True if Git Numberer is enabled on this ref."""
963 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100964 KNOWN_PROJECTS_WHITELIST = [
965 'chromium/src',
966 'external/webrtc',
967 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100968 'infra/experimental',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100969 ]
970
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100971 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
972 url_parts = urlparse.urlparse(remote_url)
973 project_name = url_parts.path.lstrip('/').rstrip('git./')
974 for known in KNOWN_PROJECTS_WHITELIST:
975 if project_name.endswith(known):
976 break
977 else:
978 # Early exit to avoid extra fetches for repos that aren't using Git
979 # Numberer.
980 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100981
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100982 with _get_gerrit_project_config_file(remote_url) as project_config_file:
983 if project_config_file is None:
984 # Failed to fetch project.config, which shouldn't happen on open source
985 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100986 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100987 def get_opts(x):
988 code, out = RunGitWithCode(
989 ['config', '-f', project_config_file, '--get-all',
990 'plugin.git-numberer.validate-%s-refglob' % x])
991 if code == 0:
992 return out.strip().splitlines()
993 return []
994 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100995
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100996 logging.info('validator config enabled %s disabled %s refglobs for '
997 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000998
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100999 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001000 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001001 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001002 return True
1003 return False
1004
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001005 if match_refglobs(disabled):
1006 return False
1007 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001008
1009
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010def ShortBranchName(branch):
1011 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001012 return branch.replace('refs/heads/', '', 1)
1013
1014
1015def GetCurrentBranchRef():
1016 """Returns branch ref (e.g., refs/heads/master) or None."""
1017 return RunGit(['symbolic-ref', 'HEAD'],
1018 stderr=subprocess2.VOID, error_ok=True).strip() or None
1019
1020
1021def GetCurrentBranch():
1022 """Returns current branch or None.
1023
1024 For refs/heads/* branches, returns just last part. For others, full ref.
1025 """
1026 branchref = GetCurrentBranchRef()
1027 if branchref:
1028 return ShortBranchName(branchref)
1029 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030
1031
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001032class _CQState(object):
1033 """Enum for states of CL with respect to Commit Queue."""
1034 NONE = 'none'
1035 DRY_RUN = 'dry_run'
1036 COMMIT = 'commit'
1037
1038 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1039
1040
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001041class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001042 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001043 self.issue = issue
1044 self.patchset = patchset
1045 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001046 assert codereview in (None, 'rietveld', 'gerrit')
1047 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001048
1049 @property
1050 def valid(self):
1051 return self.issue is not None
1052
1053
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001054def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001055 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1056 fail_result = _ParsedIssueNumberArgument()
1057
1058 if arg.isdigit():
1059 return _ParsedIssueNumberArgument(issue=int(arg))
1060 if not arg.startswith('http'):
1061 return fail_result
1062 url = gclient_utils.UpgradeToHttps(arg)
1063 try:
1064 parsed_url = urlparse.urlparse(url)
1065 except ValueError:
1066 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001067
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001068 if codereview is not None:
1069 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1070 return parsed or fail_result
1071
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001072 results = {}
1073 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1074 parsed = cls.ParseIssueURL(parsed_url)
1075 if parsed is not None:
1076 results[name] = parsed
1077
1078 if not results:
1079 return fail_result
1080 if len(results) == 1:
1081 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001082
1083 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1084 # This is likely Gerrit.
1085 return results['gerrit']
1086 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001087 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001088
1089
Aaron Gablea45ee112016-11-22 15:14:38 -08001090class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001091 def __init__(self, issue, url):
1092 self.issue = issue
1093 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001094 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001095
1096 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001097 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001098 self.issue, self.url)
1099
1100
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001101_CommentSummary = collections.namedtuple(
1102 '_CommentSummary', ['date', 'message', 'sender',
1103 # TODO(tandrii): these two aren't known in Gerrit.
1104 'approval', 'disapproval'])
1105
1106
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001107class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001108 """Changelist works with one changelist in local branch.
1109
1110 Supports two codereview backends: Rietveld or Gerrit, selected at object
1111 creation.
1112
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001113 Notes:
1114 * Not safe for concurrent multi-{thread,process} use.
1115 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001116 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001117 """
1118
1119 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1120 """Create a new ChangeList instance.
1121
1122 If issue is given, the codereview must be given too.
1123
1124 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1125 Otherwise, it's decided based on current configuration of the local branch,
1126 with default being 'rietveld' for backwards compatibility.
1127 See _load_codereview_impl for more details.
1128
1129 **kwargs will be passed directly to codereview implementation.
1130 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001132 global settings
1133 if not settings:
1134 # Happens when git_cl.py is used as a utility library.
1135 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001136
1137 if issue:
1138 assert codereview, 'codereview must be known, if issue is known'
1139
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 self.branchref = branchref
1141 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001142 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 self.branch = ShortBranchName(self.branchref)
1144 else:
1145 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001147 self.lookedup_issue = False
1148 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 self.has_description = False
1150 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001151 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153 self.cc = None
1154 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001155 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001156
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001157 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001158 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001159 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001160 assert self._codereview_impl
1161 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001162
1163 def _load_codereview_impl(self, codereview=None, **kwargs):
1164 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001165 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1166 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1167 self._codereview = codereview
1168 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169 return
1170
1171 # Automatic selection based on issue number set for a current branch.
1172 # Rietveld takes precedence over Gerrit.
1173 assert not self.issue
1174 # Whether we find issue or not, we are doing the lookup.
1175 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001176 if self.GetBranch():
1177 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1178 issue = _git_get_branch_config_value(
1179 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1180 if issue:
1181 self._codereview = codereview
1182 self._codereview_impl = cls(self, **kwargs)
1183 self.issue = int(issue)
1184 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001185
1186 # No issue is set for this branch, so decide based on repo-wide settings.
1187 return self._load_codereview_impl(
1188 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1189 **kwargs)
1190
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001191 def IsGerrit(self):
1192 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001193
1194 def GetCCList(self):
1195 """Return the users cc'd on this CL.
1196
agable92bec4f2016-08-24 09:27:27 -07001197 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001198 """
1199 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001200 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001201 more_cc = ','.join(self.watchers)
1202 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1203 return self.cc
1204
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001205 def GetCCListWithoutDefault(self):
1206 """Return the users cc'd on this CL excluding default ones."""
1207 if self.cc is None:
1208 self.cc = ','.join(self.watchers)
1209 return self.cc
1210
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001211 def SetWatchers(self, watchers):
1212 """Set the list of email addresses that should be cc'd based on the changed
1213 files in this CL.
1214 """
1215 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216
1217 def GetBranch(self):
1218 """Returns the short branch name, e.g. 'master'."""
1219 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001220 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001221 if not branchref:
1222 return None
1223 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 self.branch = ShortBranchName(self.branchref)
1225 return self.branch
1226
1227 def GetBranchRef(self):
1228 """Returns the full branch name, e.g. 'refs/heads/master'."""
1229 self.GetBranch() # Poke the lazy loader.
1230 return self.branchref
1231
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001232 def ClearBranch(self):
1233 """Clears cached branch data of this object."""
1234 self.branch = self.branchref = None
1235
tandrii5d48c322016-08-18 16:19:37 -07001236 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1237 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1238 kwargs['branch'] = self.GetBranch()
1239 return _git_get_branch_config_value(key, default, **kwargs)
1240
1241 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1242 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1243 assert self.GetBranch(), (
1244 'this CL must have an associated branch to %sset %s%s' %
1245 ('un' if value is None else '',
1246 key,
1247 '' if value is None else ' to %r' % value))
1248 kwargs['branch'] = self.GetBranch()
1249 return _git_set_branch_config_value(key, value, **kwargs)
1250
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001251 @staticmethod
1252 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001253 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 e.g. 'origin', 'refs/heads/master'
1255 """
1256 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001257 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1258
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001260 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001262 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1263 error_ok=True).strip()
1264 if upstream_branch:
1265 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001267 # Else, try to guess the origin remote.
1268 remote_branches = RunGit(['branch', '-r']).split()
1269 if 'origin/master' in remote_branches:
1270 # Fall back on origin/master if it exits.
1271 remote = 'origin'
1272 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001274 DieWithError(
1275 'Unable to determine default branch to diff against.\n'
1276 'Either pass complete "git diff"-style arguments, like\n'
1277 ' git cl upload origin/master\n'
1278 'or verify this branch is set up to track another \n'
1279 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280
1281 return remote, upstream_branch
1282
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001283 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001284 upstream_branch = self.GetUpstreamBranch()
1285 if not BranchExists(upstream_branch):
1286 DieWithError('The upstream for the current branch (%s) does not exist '
1287 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001288 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001289 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 def GetUpstreamBranch(self):
1292 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001293 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001295 upstream_branch = upstream_branch.replace('refs/heads/',
1296 'refs/remotes/%s/' % remote)
1297 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1298 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299 self.upstream_branch = upstream_branch
1300 return self.upstream_branch
1301
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001302 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001303 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001304 remote, branch = None, self.GetBranch()
1305 seen_branches = set()
1306 while branch not in seen_branches:
1307 seen_branches.add(branch)
1308 remote, branch = self.FetchUpstreamTuple(branch)
1309 branch = ShortBranchName(branch)
1310 if remote != '.' or branch.startswith('refs/remotes'):
1311 break
1312 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001313 remotes = RunGit(['remote'], error_ok=True).split()
1314 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001315 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001316 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001317 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001318 logging.warn('Could not determine which remote this change is '
1319 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001320 else:
1321 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001322 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001323 branch = 'HEAD'
1324 if branch.startswith('refs/remotes'):
1325 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001326 elif branch.startswith('refs/branch-heads/'):
1327 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001328 else:
1329 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001330 return self._remote
1331
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001332 def GitSanityChecks(self, upstream_git_obj):
1333 """Checks git repo status and ensures diff is from local commits."""
1334
sbc@chromium.org79706062015-01-14 21:18:12 +00001335 if upstream_git_obj is None:
1336 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001337 print('ERROR: unable to determine current branch (detached HEAD?)',
1338 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001339 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001340 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001341 return False
1342
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001343 # Verify the commit we're diffing against is in our current branch.
1344 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1345 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1346 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001347 print('ERROR: %s is not in the current branch. You may need to rebase '
1348 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001349 return False
1350
1351 # List the commits inside the diff, and verify they are all local.
1352 commits_in_diff = RunGit(
1353 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1354 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1355 remote_branch = remote_branch.strip()
1356 if code != 0:
1357 _, remote_branch = self.GetRemoteBranch()
1358
1359 commits_in_remote = RunGit(
1360 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1361
1362 common_commits = set(commits_in_diff) & set(commits_in_remote)
1363 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001364 print('ERROR: Your diff contains %d commits already in %s.\n'
1365 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1366 'the diff. If you are using a custom git flow, you can override'
1367 ' the reference used for this check with "git config '
1368 'gitcl.remotebranch <git-ref>".' % (
1369 len(common_commits), remote_branch, upstream_git_obj),
1370 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001371 return False
1372 return True
1373
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001374 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001375 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001376
1377 Returns None if it is not set.
1378 """
tandrii5d48c322016-08-18 16:19:37 -07001379 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001380
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381 def GetRemoteUrl(self):
1382 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1383
1384 Returns None if there is no remote.
1385 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001386 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001387 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1388
1389 # If URL is pointing to a local directory, it is probably a git cache.
1390 if os.path.isdir(url):
1391 url = RunGit(['config', 'remote.%s.url' % remote],
1392 error_ok=True,
1393 cwd=url).strip()
1394 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001396 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001397 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001398 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001399 self.issue = self._GitGetBranchConfigValue(
1400 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001401 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 return self.issue
1403
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 def GetIssueURL(self):
1405 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001406 issue = self.GetIssue()
1407 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001408 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001409 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001411 def GetDescription(self, pretty=False, force=False):
1412 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001414 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 self.has_description = True
1416 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001417 # Set width to 72 columns + 2 space indent.
1418 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001420 lines = self.description.splitlines()
1421 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 return self.description
1423
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001424 def GetDescriptionFooters(self):
1425 """Returns (non_footer_lines, footers) for the commit message.
1426
1427 Returns:
1428 non_footer_lines (list(str)) - Simple list of description lines without
1429 any footer. The lines do not contain newlines, nor does the list contain
1430 the empty line between the message and the footers.
1431 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1432 [("Change-Id", "Ideadbeef...."), ...]
1433 """
1434 raw_description = self.GetDescription()
1435 msg_lines, _, footers = git_footers.split_footers(raw_description)
1436 if footers:
1437 msg_lines = msg_lines[:len(msg_lines)-1]
1438 return msg_lines, footers
1439
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001441 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001442 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001443 self.patchset = self._GitGetBranchConfigValue(
1444 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001445 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 return self.patchset
1447
1448 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001449 """Set this branch's patchset. If patchset=0, clears the patchset."""
1450 assert self.GetBranch()
1451 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001452 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001453 else:
1454 self.patchset = int(patchset)
1455 self._GitSetBranchConfigValue(
1456 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001458 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001459 """Set this branch's issue. If issue isn't given, clears the issue."""
1460 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001462 issue = int(issue)
1463 self._GitSetBranchConfigValue(
1464 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001465 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001466 codereview_server = self._codereview_impl.GetCodereviewServer()
1467 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001468 self._GitSetBranchConfigValue(
1469 self._codereview_impl.CodereviewServerConfigKey(),
1470 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001471 else:
tandrii5d48c322016-08-18 16:19:37 -07001472 # Reset all of these just to be clean.
1473 reset_suffixes = [
1474 'last-upload-hash',
1475 self._codereview_impl.IssueConfigKey(),
1476 self._codereview_impl.PatchsetConfigKey(),
1477 self._codereview_impl.CodereviewServerConfigKey(),
1478 ] + self._PostUnsetIssueProperties()
1479 for prop in reset_suffixes:
1480 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001481 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001482 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001483
dnjba1b0f32016-09-02 12:37:42 -07001484 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001485 if not self.GitSanityChecks(upstream_branch):
1486 DieWithError('\nGit sanity check failure')
1487
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001488 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001489 if not root:
1490 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001491 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001492
1493 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001494 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001495 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001496 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001497 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001498 except subprocess2.CalledProcessError:
1499 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001500 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001501 'This branch probably doesn\'t exist anymore. To reset the\n'
1502 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001503 ' git branch --set-upstream-to origin/master %s\n'
1504 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001505 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001506
maruel@chromium.org52424302012-08-29 15:14:30 +00001507 issue = self.GetIssue()
1508 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001509 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001510 description = self.GetDescription()
1511 else:
1512 # If the change was never uploaded, use the log messages of all commits
1513 # up to the branch point, as git cl upload will prefill the description
1514 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001515 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1516 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001517
1518 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001519 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001520 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001521 name,
1522 description,
1523 absroot,
1524 files,
1525 issue,
1526 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001527 author,
1528 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001529
dsansomee2d6fd92016-09-08 00:10:47 -07001530 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001531 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001532 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001533 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001534
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001535 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1536 """Sets the description for this CL remotely.
1537
1538 You can get description_lines and footers with GetDescriptionFooters.
1539
1540 Args:
1541 description_lines (list(str)) - List of CL description lines without
1542 newline characters.
1543 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1544 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1545 `List-Of-Tokens`). It will be case-normalized so that each token is
1546 title-cased.
1547 """
1548 new_description = '\n'.join(description_lines)
1549 if footers:
1550 new_description += '\n'
1551 for k, v in footers:
1552 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1553 if not git_footers.FOOTER_PATTERN.match(foot):
1554 raise ValueError('Invalid footer %r' % foot)
1555 new_description += foot + '\n'
1556 self.UpdateDescription(new_description, force)
1557
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001558 def RunHook(self, committing, may_prompt, verbose, change):
1559 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1560 try:
1561 return presubmit_support.DoPresubmitChecks(change, committing,
1562 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1563 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001564 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1565 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001566 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001567 DieWithError(
1568 ('%s\nMaybe your depot_tools is out of date?\n'
1569 'If all fails, contact maruel@') % e)
1570
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001571 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1572 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001573 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1574 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001575 else:
1576 # Assume url.
1577 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1578 urlparse.urlparse(issue_arg))
1579 if not parsed_issue_arg or not parsed_issue_arg.valid:
1580 DieWithError('Failed to parse issue argument "%s". '
1581 'Must be an issue number or a valid URL.' % issue_arg)
1582 return self._codereview_impl.CMDPatchWithParsedIssue(
1583 parsed_issue_arg, reject, nocommit, directory)
1584
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001585 def CMDUpload(self, options, git_diff_args, orig_args):
1586 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001587 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001588 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001589 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001590 else:
1591 if self.GetBranch() is None:
1592 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1593
1594 # Default to diffing against common ancestor of upstream branch
1595 base_branch = self.GetCommonAncestorWithUpstream()
1596 git_diff_args = [base_branch, 'HEAD']
1597
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001598 # Fast best-effort checks to abort before running potentially
1599 # expensive hooks if uploading is likely to fail anyway. Passing these
1600 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001601 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001602 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001603
1604 # Apply watchlists on upload.
1605 change = self.GetChange(base_branch, None)
1606 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1607 files = [f.LocalPath() for f in change.AffectedFiles()]
1608 if not options.bypass_watchlists:
1609 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1610
1611 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001612 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001613 # Set the reviewer list now so that presubmit checks can access it.
1614 change_description = ChangeDescription(change.FullDescriptionText())
1615 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001616 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001617 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001618 change)
1619 change.SetDescriptionText(change_description.description)
1620 hook_results = self.RunHook(committing=False,
1621 may_prompt=not options.force,
1622 verbose=options.verbose,
1623 change=change)
1624 if not hook_results.should_continue():
1625 return 1
1626 if not options.reviewers and hook_results.reviewers:
1627 options.reviewers = hook_results.reviewers.split(',')
1628
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001629 # TODO(tandrii): Checking local patchset against remote patchset is only
1630 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1631 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001632 latest_patchset = self.GetMostRecentPatchset()
1633 local_patchset = self.GetPatchset()
1634 if (latest_patchset and local_patchset and
1635 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001636 print('The last upload made from this repository was patchset #%d but '
1637 'the most recent patchset on the server is #%d.'
1638 % (local_patchset, latest_patchset))
1639 print('Uploading will still work, but if you\'ve uploaded to this '
1640 'issue from another machine or branch the patch you\'re '
1641 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001642 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001643
1644 print_stats(options.similarity, options.find_copies, git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001645 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001646 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001647 if options.use_commit_queue:
1648 self.SetCQState(_CQState.COMMIT)
1649 elif options.cq_dry_run:
1650 self.SetCQState(_CQState.DRY_RUN)
1651
tandrii5d48c322016-08-18 16:19:37 -07001652 _git_set_branch_config_value('last-upload-hash',
1653 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001654 # Run post upload hooks, if specified.
1655 if settings.GetRunPostUploadHook():
1656 presubmit_support.DoPostUploadExecuter(
1657 change,
1658 self,
1659 settings.GetRoot(),
1660 options.verbose,
1661 sys.stdout)
1662
1663 # Upload all dependencies if specified.
1664 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001665 print()
1666 print('--dependencies has been specified.')
1667 print('All dependent local branches will be re-uploaded.')
1668 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001669 # Remove the dependencies flag from args so that we do not end up in a
1670 # loop.
1671 orig_args.remove('--dependencies')
1672 ret = upload_branch_deps(self, orig_args)
1673 return ret
1674
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001675 def SetCQState(self, new_state):
1676 """Update the CQ state for latest patchset.
1677
1678 Issue must have been already uploaded and known.
1679 """
1680 assert new_state in _CQState.ALL_STATES
1681 assert self.GetIssue()
1682 return self._codereview_impl.SetCQState(new_state)
1683
qyearsley1fdfcb62016-10-24 13:22:03 -07001684 def TriggerDryRun(self):
1685 """Triggers a dry run and prints a warning on failure."""
1686 # TODO(qyearsley): Either re-use this method in CMDset_commit
1687 # and CMDupload, or change CMDtry to trigger dry runs with
1688 # just SetCQState, and catch keyboard interrupt and other
1689 # errors in that method.
1690 try:
1691 self.SetCQState(_CQState.DRY_RUN)
1692 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1693 return 0
1694 except KeyboardInterrupt:
1695 raise
1696 except:
1697 print('WARNING: failed to trigger CQ Dry Run.\n'
1698 'Either:\n'
1699 ' * your project has no CQ\n'
1700 ' * you don\'t have permission to trigger Dry Run\n'
1701 ' * bug in this code (see stack trace below).\n'
1702 'Consider specifying which bots to trigger manually '
1703 'or asking your project owners for permissions '
1704 'or contacting Chrome Infrastructure team at '
1705 'https://www.chromium.org/infra\n\n')
1706 # Still raise exception so that stack trace is printed.
1707 raise
1708
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001709 # Forward methods to codereview specific implementation.
1710
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001711 def AddComment(self, message):
1712 return self._codereview_impl.AddComment(message)
1713
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001714 def GetCommentsSummary(self):
1715 """Returns list of _CommentSummary for each comment.
1716
1717 Note: comments per file or per line are not included,
1718 only top-level comments are returned.
1719 """
1720 return self._codereview_impl.GetCommentsSummary()
1721
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001722 def CloseIssue(self):
1723 return self._codereview_impl.CloseIssue()
1724
1725 def GetStatus(self):
1726 return self._codereview_impl.GetStatus()
1727
1728 def GetCodereviewServer(self):
1729 return self._codereview_impl.GetCodereviewServer()
1730
tandriide281ae2016-10-12 06:02:30 -07001731 def GetIssueOwner(self):
1732 """Get owner from codereview, which may differ from this checkout."""
1733 return self._codereview_impl.GetIssueOwner()
1734
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001735 def GetMostRecentPatchset(self):
1736 return self._codereview_impl.GetMostRecentPatchset()
1737
tandriide281ae2016-10-12 06:02:30 -07001738 def CannotTriggerTryJobReason(self):
1739 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1740 return self._codereview_impl.CannotTriggerTryJobReason()
1741
tandrii8c5a3532016-11-04 07:52:02 -07001742 def GetTryjobProperties(self, patchset=None):
1743 """Returns dictionary of properties to launch tryjob."""
1744 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1745
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001746 def __getattr__(self, attr):
1747 # This is because lots of untested code accesses Rietveld-specific stuff
1748 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001749 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001750 # Note that child method defines __getattr__ as well, and forwards it here,
1751 # because _RietveldChangelistImpl is not cleaned up yet, and given
1752 # deprecation of Rietveld, it should probably be just removed.
1753 # Until that time, avoid infinite recursion by bypassing __getattr__
1754 # of implementation class.
1755 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001756
1757
1758class _ChangelistCodereviewBase(object):
1759 """Abstract base class encapsulating codereview specifics of a changelist."""
1760 def __init__(self, changelist):
1761 self._changelist = changelist # instance of Changelist
1762
1763 def __getattr__(self, attr):
1764 # Forward methods to changelist.
1765 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1766 # _RietveldChangelistImpl to avoid this hack?
1767 return getattr(self._changelist, attr)
1768
1769 def GetStatus(self):
1770 """Apply a rough heuristic to give a simple summary of an issue's review
1771 or CQ status, assuming adherence to a common workflow.
1772
1773 Returns None if no issue for this branch, or specific string keywords.
1774 """
1775 raise NotImplementedError()
1776
1777 def GetCodereviewServer(self):
1778 """Returns server URL without end slash, like "https://codereview.com"."""
1779 raise NotImplementedError()
1780
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001781 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001782 """Fetches and returns description from the codereview server."""
1783 raise NotImplementedError()
1784
tandrii5d48c322016-08-18 16:19:37 -07001785 @classmethod
1786 def IssueConfigKey(cls):
1787 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001788 raise NotImplementedError()
1789
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001790 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001791 def PatchsetConfigKey(cls):
1792 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001793 raise NotImplementedError()
1794
tandrii5d48c322016-08-18 16:19:37 -07001795 @classmethod
1796 def CodereviewServerConfigKey(cls):
1797 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001798 raise NotImplementedError()
1799
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001800 def _PostUnsetIssueProperties(self):
1801 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001802 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001803
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001804 def GetRieveldObjForPresubmit(self):
1805 # This is an unfortunate Rietveld-embeddedness in presubmit.
1806 # For non-Rietveld codereviews, this probably should return a dummy object.
1807 raise NotImplementedError()
1808
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001809 def GetGerritObjForPresubmit(self):
1810 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1811 return None
1812
dsansomee2d6fd92016-09-08 00:10:47 -07001813 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001814 """Update the description on codereview site."""
1815 raise NotImplementedError()
1816
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001817 def AddComment(self, message):
1818 """Posts a comment to the codereview site."""
1819 raise NotImplementedError()
1820
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001821 def GetCommentsSummary(self):
1822 raise NotImplementedError()
1823
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001824 def CloseIssue(self):
1825 """Closes the issue."""
1826 raise NotImplementedError()
1827
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001828 def GetMostRecentPatchset(self):
1829 """Returns the most recent patchset number from the codereview site."""
1830 raise NotImplementedError()
1831
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001832 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1833 directory):
1834 """Fetches and applies the issue.
1835
1836 Arguments:
1837 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1838 reject: if True, reject the failed patch instead of switching to 3-way
1839 merge. Rietveld only.
1840 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1841 only.
1842 directory: switch to directory before applying the patch. Rietveld only.
1843 """
1844 raise NotImplementedError()
1845
1846 @staticmethod
1847 def ParseIssueURL(parsed_url):
1848 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1849 failed."""
1850 raise NotImplementedError()
1851
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001852 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001853 """Best effort check that user is authenticated with codereview server.
1854
1855 Arguments:
1856 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001857 refresh: whether to attempt to refresh credentials. Ignored if not
1858 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001859 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001860 raise NotImplementedError()
1861
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001862 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001863 """Best effort check that uploading isn't supposed to fail for predictable
1864 reasons.
1865
1866 This method should raise informative exception if uploading shouldn't
1867 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001868
1869 Arguments:
1870 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001871 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001872 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001873
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001874 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001875 """Uploads a change to codereview."""
1876 raise NotImplementedError()
1877
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001878 def SetCQState(self, new_state):
1879 """Update the CQ state for latest patchset.
1880
1881 Issue must have been already uploaded and known.
1882 """
1883 raise NotImplementedError()
1884
tandriie113dfd2016-10-11 10:20:12 -07001885 def CannotTriggerTryJobReason(self):
1886 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1887 raise NotImplementedError()
1888
tandriide281ae2016-10-12 06:02:30 -07001889 def GetIssueOwner(self):
1890 raise NotImplementedError()
1891
tandrii8c5a3532016-11-04 07:52:02 -07001892 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001893 raise NotImplementedError()
1894
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001895
1896class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001897 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001898 super(_RietveldChangelistImpl, self).__init__(changelist)
1899 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001900 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001901 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001902
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001903 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001904 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001905 self._props = None
1906 self._rpc_server = None
1907
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001908 def GetCodereviewServer(self):
1909 if not self._rietveld_server:
1910 # If we're on a branch then get the server potentially associated
1911 # with that branch.
1912 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001913 self._rietveld_server = gclient_utils.UpgradeToHttps(
1914 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001915 if not self._rietveld_server:
1916 self._rietveld_server = settings.GetDefaultServerUrl()
1917 return self._rietveld_server
1918
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001919 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001920 """Best effort check that user is authenticated with Rietveld server."""
1921 if self._auth_config.use_oauth2:
1922 authenticator = auth.get_authenticator_for_host(
1923 self.GetCodereviewServer(), self._auth_config)
1924 if not authenticator.has_cached_credentials():
1925 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001926 if refresh:
1927 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001928
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001929 def EnsureCanUploadPatchset(self, force):
1930 # No checks for Rietveld because we are deprecating Rietveld.
1931 pass
1932
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001933 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001934 issue = self.GetIssue()
1935 assert issue
1936 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001937 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001938 except urllib2.HTTPError as e:
1939 if e.code == 404:
1940 DieWithError(
1941 ('\nWhile fetching the description for issue %d, received a '
1942 '404 (not found)\n'
1943 'error. It is likely that you deleted this '
1944 'issue on the server. If this is the\n'
1945 'case, please run\n\n'
1946 ' git cl issue 0\n\n'
1947 'to clear the association with the deleted issue. Then run '
1948 'this command again.') % issue)
1949 else:
1950 DieWithError(
1951 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1952 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001953 print('Warning: Failed to retrieve CL description due to network '
1954 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001955 return ''
1956
1957 def GetMostRecentPatchset(self):
1958 return self.GetIssueProperties()['patchsets'][-1]
1959
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001960 def GetIssueProperties(self):
1961 if self._props is None:
1962 issue = self.GetIssue()
1963 if not issue:
1964 self._props = {}
1965 else:
1966 self._props = self.RpcServer().get_issue_properties(issue, True)
1967 return self._props
1968
tandriie113dfd2016-10-11 10:20:12 -07001969 def CannotTriggerTryJobReason(self):
1970 props = self.GetIssueProperties()
1971 if not props:
1972 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1973 if props.get('closed'):
1974 return 'CL %s is closed' % self.GetIssue()
1975 if props.get('private'):
1976 return 'CL %s is private' % self.GetIssue()
1977 return None
1978
tandrii8c5a3532016-11-04 07:52:02 -07001979 def GetTryjobProperties(self, patchset=None):
1980 """Returns dictionary of properties to launch tryjob."""
1981 project = (self.GetIssueProperties() or {}).get('project')
1982 return {
1983 'issue': self.GetIssue(),
1984 'patch_project': project,
1985 'patch_storage': 'rietveld',
1986 'patchset': patchset or self.GetPatchset(),
1987 'rietveld': self.GetCodereviewServer(),
1988 }
1989
tandriide281ae2016-10-12 06:02:30 -07001990 def GetIssueOwner(self):
1991 return (self.GetIssueProperties() or {}).get('owner_email')
1992
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001993 def AddComment(self, message):
1994 return self.RpcServer().add_comment(self.GetIssue(), message)
1995
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001996 def GetCommentsSummary(self):
1997 summary = []
1998 for message in self.GetIssueProperties().get('messages', []):
1999 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2000 summary.append(_CommentSummary(
2001 date=date,
2002 disapproval=bool(message['disapproval']),
2003 approval=bool(message['approval']),
2004 sender=message['sender'],
2005 message=message['text'],
2006 ))
2007 return summary
2008
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002009 def GetStatus(self):
2010 """Apply a rough heuristic to give a simple summary of an issue's review
2011 or CQ status, assuming adherence to a common workflow.
2012
2013 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002014 * 'error' - error from review tool (including deleted issues)
2015 * 'unsent' - not sent for review
2016 * 'waiting' - waiting for review
2017 * 'reply' - waiting for owner to reply to review
2018 * 'not lgtm' - Code-Review label has been set negatively
2019 * 'lgtm' - LGTM from at least one approved reviewer
2020 * 'commit' - in the commit queue
2021 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002022 """
2023 if not self.GetIssue():
2024 return None
2025
2026 try:
2027 props = self.GetIssueProperties()
2028 except urllib2.HTTPError:
2029 return 'error'
2030
2031 if props.get('closed'):
2032 # Issue is closed.
2033 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002034 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002035 # Issue is in the commit queue.
2036 return 'commit'
2037
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002038 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002039 if not messages:
2040 # No message was sent.
2041 return 'unsent'
2042
2043 if get_approving_reviewers(props):
2044 return 'lgtm'
2045 elif get_approving_reviewers(props, disapproval=True):
2046 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002047
tandrii9d2c7a32016-06-22 03:42:45 -07002048 # Skip CQ messages that don't require owner's action.
2049 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2050 if 'Dry run:' in messages[-1]['text']:
2051 messages.pop()
2052 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2053 # This message always follows prior messages from CQ,
2054 # so skip this too.
2055 messages.pop()
2056 else:
2057 # This is probably a CQ messages warranting user attention.
2058 break
2059
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002060 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002061 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002062 return 'reply'
2063 return 'waiting'
2064
dsansomee2d6fd92016-09-08 00:10:47 -07002065 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002066 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002067
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002069 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002071 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002072 return self.SetFlags({flag: value})
2073
2074 def SetFlags(self, flags):
2075 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002076 """
phajdan.jr68598232016-08-10 03:28:28 -07002077 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002078 try:
tandrii4b233bd2016-07-06 03:50:29 -07002079 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002080 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002081 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002082 if e.code == 404:
2083 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2084 if e.code == 403:
2085 DieWithError(
2086 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002087 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002088 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002089
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002090 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002091 """Returns an upload.RpcServer() to access this review's rietveld instance.
2092 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002093 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002094 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002095 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002096 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002097 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002099 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002100 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002101 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002102
tandrii5d48c322016-08-18 16:19:37 -07002103 @classmethod
2104 def PatchsetConfigKey(cls):
2105 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106
tandrii5d48c322016-08-18 16:19:37 -07002107 @classmethod
2108 def CodereviewServerConfigKey(cls):
2109 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002110
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002111 def GetRieveldObjForPresubmit(self):
2112 return self.RpcServer()
2113
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002114 def SetCQState(self, new_state):
2115 props = self.GetIssueProperties()
2116 if props.get('private'):
2117 DieWithError('Cannot set-commit on private issue')
2118
2119 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002120 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002121 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002122 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002123 else:
tandrii4b233bd2016-07-06 03:50:29 -07002124 assert new_state == _CQState.DRY_RUN
2125 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002126
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002127 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2128 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002129 # PatchIssue should never be called with a dirty tree. It is up to the
2130 # caller to check this, but just in case we assert here since the
2131 # consequences of the caller not checking this could be dire.
2132 assert(not git_common.is_dirty_git_tree('apply'))
2133 assert(parsed_issue_arg.valid)
2134 self._changelist.issue = parsed_issue_arg.issue
2135 if parsed_issue_arg.hostname:
2136 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2137
skobes6468b902016-10-24 08:45:10 -07002138 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2139 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2140 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002141 try:
skobes6468b902016-10-24 08:45:10 -07002142 scm_obj.apply_patch(patchset_object)
2143 except Exception as e:
2144 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002145 return 1
2146
2147 # If we had an issue, commit the current state and register the issue.
2148 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002149 self.SetIssue(self.GetIssue())
2150 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002151 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2152 'patch from issue %(i)s at patchset '
2153 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2154 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002155 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002156 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002157 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002158 return 0
2159
2160 @staticmethod
2161 def ParseIssueURL(parsed_url):
2162 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2163 return None
wychen3c1c1722016-08-04 11:46:36 -07002164 # Rietveld patch: https://domain/<number>/#ps<patchset>
2165 match = re.match(r'/(\d+)/$', parsed_url.path)
2166 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2167 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002168 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002169 issue=int(match.group(1)),
2170 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002171 hostname=parsed_url.netloc,
2172 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002173 # Typical url: https://domain/<issue_number>[/[other]]
2174 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2175 if match:
skobes6468b902016-10-24 08:45:10 -07002176 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002177 issue=int(match.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 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2181 match = re.match(r'/download/issue(\d+)_(\d+).diff$', 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)),
2185 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002186 hostname=parsed_url.netloc,
2187 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002188 return None
2189
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002190 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002191 """Upload the patch to Rietveld."""
2192 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2193 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002194 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2195 if options.emulate_svn_auto_props:
2196 upload_args.append('--emulate_svn_auto_props')
2197
2198 change_desc = None
2199
2200 if options.email is not None:
2201 upload_args.extend(['--email', options.email])
2202
2203 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002204 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002205 upload_args.extend(['--title', options.title])
2206 if options.message:
2207 upload_args.extend(['--message', options.message])
2208 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002209 print('This branch is associated with issue %s. '
2210 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211 else:
nodirca166002016-06-27 10:59:51 -07002212 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002213 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002214 if options.message:
2215 message = options.message
2216 else:
2217 message = CreateDescriptionFromLog(args)
2218 if options.title:
2219 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002221 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002222 change_desc.update_reviewers(options.reviewers, options.tbrs,
2223 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002224 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002225 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002226
2227 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002228 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002229 return 1
2230
2231 upload_args.extend(['--message', change_desc.description])
2232 if change_desc.get_reviewers():
2233 upload_args.append('--reviewers=%s' % ','.join(
2234 change_desc.get_reviewers()))
2235 if options.send_mail:
2236 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002237 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002238 upload_args.append('--send_mail')
2239
2240 # We check this before applying rietveld.private assuming that in
2241 # rietveld.cc only addresses which we can send private CLs to are listed
2242 # if rietveld.private is set, and so we should ignore rietveld.cc only
2243 # when --private is specified explicitly on the command line.
2244 if options.private:
2245 logging.warn('rietveld.cc is ignored since private flag is specified. '
2246 'You need to review and add them manually if necessary.')
2247 cc = self.GetCCListWithoutDefault()
2248 else:
2249 cc = self.GetCCList()
2250 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002251 if change_desc.get_cced():
2252 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002253 if cc:
2254 upload_args.extend(['--cc', cc])
2255
2256 if options.private or settings.GetDefaultPrivateFlag() == "True":
2257 upload_args.append('--private')
2258
2259 upload_args.extend(['--git_similarity', str(options.similarity)])
2260 if not options.find_copies:
2261 upload_args.extend(['--git_no_find_copies'])
2262
2263 # Include the upstream repo's URL in the change -- this is useful for
2264 # projects that have their source spread across multiple repos.
2265 remote_url = self.GetGitBaseUrlFromConfig()
2266 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002267 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2268 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2269 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002270 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002271 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002272 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002273 if target_ref:
2274 upload_args.extend(['--target_ref', target_ref])
2275
2276 # Look for dependent patchsets. See crbug.com/480453 for more details.
2277 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2278 upstream_branch = ShortBranchName(upstream_branch)
2279 if remote is '.':
2280 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002281 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002282 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002283 print()
2284 print('Skipping dependency patchset upload because git config '
2285 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2286 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002287 else:
2288 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002289 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002290 auth_config=auth_config)
2291 branch_cl_issue_url = branch_cl.GetIssueURL()
2292 branch_cl_issue = branch_cl.GetIssue()
2293 branch_cl_patchset = branch_cl.GetPatchset()
2294 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2295 upload_args.extend(
2296 ['--depends_on_patchset', '%s:%s' % (
2297 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002298 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002299 '\n'
2300 'The current branch (%s) is tracking a local branch (%s) with '
2301 'an associated CL.\n'
2302 'Adding %s/#ps%s as a dependency patchset.\n'
2303 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2304 branch_cl_patchset))
2305
2306 project = settings.GetProject()
2307 if project:
2308 upload_args.extend(['--project', project])
2309
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002310 try:
2311 upload_args = ['upload'] + upload_args + args
2312 logging.info('upload.RealMain(%s)', upload_args)
2313 issue, patchset = upload.RealMain(upload_args)
2314 issue = int(issue)
2315 patchset = int(patchset)
2316 except KeyboardInterrupt:
2317 sys.exit(1)
2318 except:
2319 # If we got an exception after the user typed a description for their
2320 # change, back up the description before re-raising.
2321 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002322 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002323 raise
2324
2325 if not self.GetIssue():
2326 self.SetIssue(issue)
2327 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002328 return 0
2329
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002330
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002331class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002332 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002333 # auth_config is Rietveld thing, kept here to preserve interface only.
2334 super(_GerritChangelistImpl, self).__init__(changelist)
2335 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002336 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002337 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002338 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002339 # Map from change number (issue) to its detail cache.
2340 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002341
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002342 if codereview_host is not None:
2343 assert not codereview_host.startswith('https://'), codereview_host
2344 self._gerrit_host = codereview_host
2345 self._gerrit_server = 'https://%s' % codereview_host
2346
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002347 def _GetGerritHost(self):
2348 # Lazy load of configs.
2349 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002350 if self._gerrit_host and '.' not in self._gerrit_host:
2351 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2352 # This happens for internal stuff http://crbug.com/614312.
2353 parsed = urlparse.urlparse(self.GetRemoteUrl())
2354 if parsed.scheme == 'sso':
2355 print('WARNING: using non https URLs for remote is likely broken\n'
2356 ' Your current remote is: %s' % self.GetRemoteUrl())
2357 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2358 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002359 return self._gerrit_host
2360
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002361 def _GetGitHost(self):
2362 """Returns git host to be used when uploading change to Gerrit."""
2363 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2364
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002365 def GetCodereviewServer(self):
2366 if not self._gerrit_server:
2367 # If we're on a branch then get the server potentially associated
2368 # with that branch.
2369 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002370 self._gerrit_server = self._GitGetBranchConfigValue(
2371 self.CodereviewServerConfigKey())
2372 if self._gerrit_server:
2373 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002374 if not self._gerrit_server:
2375 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2376 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002377 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002378 parts[0] = parts[0] + '-review'
2379 self._gerrit_host = '.'.join(parts)
2380 self._gerrit_server = 'https://%s' % self._gerrit_host
2381 return self._gerrit_server
2382
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002383 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002384 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002385 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002386
tandrii5d48c322016-08-18 16:19:37 -07002387 @classmethod
2388 def PatchsetConfigKey(cls):
2389 return 'gerritpatchset'
2390
2391 @classmethod
2392 def CodereviewServerConfigKey(cls):
2393 return 'gerritserver'
2394
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002395 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002396 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002397 if settings.GetGerritSkipEnsureAuthenticated():
2398 # For projects with unusual authentication schemes.
2399 # See http://crbug.com/603378.
2400 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002401 # Lazy-loader to identify Gerrit and Git hosts.
2402 if gerrit_util.GceAuthenticator.is_gce():
2403 return
2404 self.GetCodereviewServer()
2405 git_host = self._GetGitHost()
2406 assert self._gerrit_server and self._gerrit_host
2407 cookie_auth = gerrit_util.CookiesAuthenticator()
2408
2409 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2410 git_auth = cookie_auth.get_auth_header(git_host)
2411 if gerrit_auth and git_auth:
2412 if gerrit_auth == git_auth:
2413 return
2414 print((
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002415 'WARNING: you have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002416 ' %s\n'
2417 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002418 ' Consider running the following command:\n'
2419 ' git cl creds-check\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002420 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002421 (git_host, self._gerrit_host,
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002422 cookie_auth.get_new_password_message(git_host)))
2423 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002424 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002425 return
2426 else:
2427 missing = (
2428 [] if gerrit_auth else [self._gerrit_host] +
2429 [] if git_auth else [git_host])
2430 DieWithError('Credentials for the following hosts are required:\n'
2431 ' %s\n'
2432 'These are read from %s (or legacy %s)\n'
2433 '%s' % (
2434 '\n '.join(missing),
2435 cookie_auth.get_gitcookies_path(),
2436 cookie_auth.get_netrc_path(),
2437 cookie_auth.get_new_password_message(git_host)))
2438
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002439 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002440 if not self.GetIssue():
2441 return
2442
2443 # Warm change details cache now to avoid RPCs later, reducing latency for
2444 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002445 self._GetChangeDetail(
2446 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002447
2448 status = self._GetChangeDetail()['status']
2449 if status in ('MERGED', 'ABANDONED'):
2450 DieWithError('Change %s has been %s, new uploads are not allowed' %
2451 (self.GetIssueURL(),
2452 'submitted' if status == 'MERGED' else 'abandoned'))
2453
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002454 if gerrit_util.GceAuthenticator.is_gce():
2455 return
2456 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2457 self._GetGerritHost())
2458 if self.GetIssueOwner() == cookies_user:
2459 return
2460 logging.debug('change %s owner is %s, cookies user is %s',
2461 self.GetIssue(), self.GetIssueOwner(), cookies_user)
2462 # Maybe user has linked accounts or smth like that,
2463 # so ask what Gerrit thinks of this user.
2464 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2465 if details['email'] == self.GetIssueOwner():
2466 return
2467 if not force:
2468 print('WARNING: change %s is owned by %s, but you authenticate to Gerrit '
2469 'as %s.\n'
2470 'Uploading may fail due to lack of permissions.' %
2471 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2472 confirm_or_exit(action='upload')
2473
2474
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002475 def _PostUnsetIssueProperties(self):
2476 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002477 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002478
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002479 def GetRieveldObjForPresubmit(self):
2480 class ThisIsNotRietveldIssue(object):
2481 def __nonzero__(self):
2482 # This is a hack to make presubmit_support think that rietveld is not
2483 # defined, yet still ensure that calls directly result in a decent
2484 # exception message below.
2485 return False
2486
2487 def __getattr__(self, attr):
2488 print(
2489 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2490 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2491 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2492 'or use Rietveld for codereview.\n'
2493 'See also http://crbug.com/579160.' % attr)
2494 raise NotImplementedError()
2495 return ThisIsNotRietveldIssue()
2496
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002497 def GetGerritObjForPresubmit(self):
2498 return presubmit_support.GerritAccessor(self._GetGerritHost())
2499
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002500 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002501 """Apply a rough heuristic to give a simple summary of an issue's review
2502 or CQ status, assuming adherence to a common workflow.
2503
2504 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002505 * 'error' - error from review tool (including deleted issues)
2506 * 'unsent' - no reviewers added
2507 * 'waiting' - waiting for review
2508 * 'reply' - waiting for uploader to reply to review
2509 * 'lgtm' - Code-Review label has been set
2510 * 'commit' - in the commit queue
2511 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002512 """
2513 if not self.GetIssue():
2514 return None
2515
2516 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002517 data = self._GetChangeDetail([
2518 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002519 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002520 return 'error'
2521
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002522 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002523 return 'closed'
2524
Aaron Gable9ab38c62017-04-06 14:36:33 -07002525 if data['labels'].get('Commit-Queue', {}).get('approved'):
2526 # The section will have an "approved" subsection if anyone has voted
2527 # the maximum value on the label.
2528 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002529
Aaron Gable9ab38c62017-04-06 14:36:33 -07002530 if data['labels'].get('Code-Review', {}).get('approved'):
2531 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002532
2533 if not data.get('reviewers', {}).get('REVIEWER', []):
2534 return 'unsent'
2535
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002536 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002537 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2538 last_message_author = messages.pop().get('author', {})
2539 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002540 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2541 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002542 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002543 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002544 if last_message_author.get('_account_id') == owner:
2545 # Most recent message was by owner.
2546 return 'waiting'
2547 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002548 # Some reply from non-owner.
2549 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002550
2551 # Somehow there are no messages even though there are reviewers.
2552 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002553
2554 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002555 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002556 return data['revisions'][data['current_revision']]['_number']
2557
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002558 def FetchDescription(self, force=False):
2559 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2560 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002561 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002562 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002563
dsansomee2d6fd92016-09-08 00:10:47 -07002564 def UpdateDescriptionRemote(self, description, force=False):
2565 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2566 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002567 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002568 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002569 'unpublished edit. Either publish the edit in the Gerrit web UI '
2570 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002571
2572 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2573 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002574 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002575 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002576
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002577 def AddComment(self, message):
2578 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2579 msg=message)
2580
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002581 def GetCommentsSummary(self):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002582 # DETAILED_ACCOUNTS is to get emails in accounts.
2583 data = self._GetChangeDetail(options=['MESSAGES', 'DETAILED_ACCOUNTS'])
2584 summary = []
2585 for msg in data.get('messages', []):
2586 # Gerrit spits out nanoseconds.
2587 assert len(msg['date'].split('.')[-1]) == 9
2588 date = datetime.datetime.strptime(msg['date'][:-3],
2589 '%Y-%m-%d %H:%M:%S.%f')
2590 summary.append(_CommentSummary(
2591 date=date,
2592 message=msg['message'],
2593 sender=msg['author']['email'],
2594 # These could be inferred from the text messages and correlated with
2595 # Code-Review label maximum, however this is not reliable.
2596 # Leaving as is until the need arises.
2597 approval=False,
2598 disapproval=False,
2599 ))
2600 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002601
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002602 def CloseIssue(self):
2603 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2604
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002605 def SubmitIssue(self, wait_for_merge=True):
2606 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2607 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002608
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002609 def _GetChangeDetail(self, options=None, issue=None,
2610 no_cache=False):
2611 """Returns details of the issue by querying Gerrit and caching results.
2612
2613 If fresh data is needed, set no_cache=True which will clear cache and
2614 thus new data will be fetched from Gerrit.
2615 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002616 options = options or []
2617 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002618 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002619
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002620 # Optimization to avoid multiple RPCs:
2621 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2622 'CURRENT_COMMIT' not in options):
2623 options.append('CURRENT_COMMIT')
2624
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002625 # Normalize issue and options for consistent keys in cache.
2626 issue = str(issue)
2627 options = [o.upper() for o in options]
2628
2629 # Check in cache first unless no_cache is True.
2630 if no_cache:
2631 self._detail_cache.pop(issue, None)
2632 else:
2633 options_set = frozenset(options)
2634 for cached_options_set, data in self._detail_cache.get(issue, []):
2635 # Assumption: data fetched before with extra options is suitable
2636 # for return for a smaller set of options.
2637 # For example, if we cached data for
2638 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2639 # and request is for options=[CURRENT_REVISION],
2640 # THEN we can return prior cached data.
2641 if options_set.issubset(cached_options_set):
2642 return data
2643
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002644 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002645 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002646 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002647 except gerrit_util.GerritError as e:
2648 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002649 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002650 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002651
2652 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002653 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002654
agable32978d92016-11-01 12:55:02 -07002655 def _GetChangeCommit(self, issue=None):
2656 issue = issue or self.GetIssue()
2657 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002658 try:
2659 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2660 except gerrit_util.GerritError as e:
2661 if e.http_status == 404:
2662 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2663 raise
agable32978d92016-11-01 12:55:02 -07002664 return data
2665
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002666 def CMDLand(self, force, bypass_hooks, verbose):
2667 if git_common.is_dirty_git_tree('land'):
2668 return 1
tandriid60367b2016-06-22 05:25:12 -07002669 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2670 if u'Commit-Queue' in detail.get('labels', {}):
2671 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002672 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2673 'which can test and land changes for you. '
2674 'Are you sure you wish to bypass it?\n',
2675 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002676
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002677 differs = True
tandriic4344b52016-08-29 06:04:54 -07002678 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002679 # Note: git diff outputs nothing if there is no diff.
2680 if not last_upload or RunGit(['diff', last_upload]).strip():
2681 print('WARNING: some changes from local branch haven\'t been uploaded')
2682 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002683 if detail['current_revision'] == last_upload:
2684 differs = False
2685 else:
2686 print('WARNING: local branch contents differ from latest uploaded '
2687 'patchset')
2688 if differs:
2689 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002690 confirm_or_exit(
2691 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2692 action='submit')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002693 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2694 elif not bypass_hooks:
2695 hook_results = self.RunHook(
2696 committing=True,
2697 may_prompt=not force,
2698 verbose=verbose,
2699 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2700 if not hook_results.should_continue():
2701 return 1
2702
2703 self.SubmitIssue(wait_for_merge=True)
2704 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002705 links = self._GetChangeCommit().get('web_links', [])
2706 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002707 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002708 print('Landed as %s' % link.get('url'))
2709 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002710 return 0
2711
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002712 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2713 directory):
2714 assert not reject
2715 assert not nocommit
2716 assert not directory
2717 assert parsed_issue_arg.valid
2718
2719 self._changelist.issue = parsed_issue_arg.issue
2720
2721 if parsed_issue_arg.hostname:
2722 self._gerrit_host = parsed_issue_arg.hostname
2723 self._gerrit_server = 'https://%s' % self._gerrit_host
2724
tandriic2405f52016-10-10 08:13:15 -07002725 try:
2726 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002727 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002728 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002729
2730 if not parsed_issue_arg.patchset:
2731 # Use current revision by default.
2732 revision_info = detail['revisions'][detail['current_revision']]
2733 patchset = int(revision_info['_number'])
2734 else:
2735 patchset = parsed_issue_arg.patchset
2736 for revision_info in detail['revisions'].itervalues():
2737 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2738 break
2739 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002740 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002741 (parsed_issue_arg.patchset, self.GetIssue()))
2742
2743 fetch_info = revision_info['fetch']['http']
2744 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002745 self.SetIssue(self.GetIssue())
2746 self.SetPatchset(patchset)
Aaron Gabled343c632017-03-15 11:02:26 -07002747 RunGit(['cherry-pick', 'FETCH_HEAD'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002748 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002749 (self.GetIssue(), self.GetPatchset()))
2750 return 0
2751
2752 @staticmethod
2753 def ParseIssueURL(parsed_url):
2754 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2755 return None
2756 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2757 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2758 # Short urls like https://domain/<issue_number> can be used, but don't allow
2759 # specifying the patchset (you'd 404), but we allow that here.
2760 if parsed_url.path == '/':
2761 part = parsed_url.fragment
2762 else:
2763 part = parsed_url.path
2764 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2765 if match:
2766 return _ParsedIssueNumberArgument(
2767 issue=int(match.group(2)),
2768 patchset=int(match.group(4)) if match.group(4) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002769 hostname=parsed_url.netloc,
2770 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002771 return None
2772
tandrii16e0b4e2016-06-07 10:34:28 -07002773 def _GerritCommitMsgHookCheck(self, offer_removal):
2774 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2775 if not os.path.exists(hook):
2776 return
2777 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2778 # custom developer made one.
2779 data = gclient_utils.FileRead(hook)
2780 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2781 return
2782 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002783 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002784 'and may interfere with it in subtle ways.\n'
2785 'We recommend you remove the commit-msg hook.')
2786 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002787 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002788 gclient_utils.rm_file_or_tree(hook)
2789 print('Gerrit commit-msg hook removed.')
2790 else:
2791 print('OK, will keep Gerrit commit-msg hook in place.')
2792
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002793 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002794 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002795 if options.squash and options.no_squash:
2796 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002797
2798 if not options.squash and not options.no_squash:
2799 # Load default for user, repo, squash=true, in this order.
2800 options.squash = settings.GetSquashGerritUploads()
2801 elif options.no_squash:
2802 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002803
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002804 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002805 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002806
Aaron Gableb56ad332017-01-06 15:24:31 -08002807 # This may be None; default fallback value is determined in logic below.
2808 title = options.title
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002809 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002810
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002811 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002812 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002813 if self.GetIssue():
2814 # Try to get the message from a previous upload.
2815 message = self.GetDescription()
2816 if not message:
2817 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002818 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002819 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002820 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002821 if options.message:
2822 # For compatibility with Rietveld, if -m|--message is given on
2823 # command line, title should be the first line of that message,
2824 # which shouldn't be confused with CL description.
2825 default_title = options.message.strip().split()[0]
2826 else:
2827 default_title = RunGit(
2828 ['show', '-s', '--format=%s', 'HEAD']).strip()
Andrii Shyshkalove00a29b2017-04-10 14:48:28 +02002829 if options.force:
2830 title = default_title
2831 else:
2832 title = ask_for_data(
2833 'Title for patchset [%s]: ' % default_title) or default_title
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002834 if title == default_title:
2835 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002836 change_id = self._GetChangeDetail()['change_id']
2837 while True:
2838 footer_change_ids = git_footers.get_footer_change_id(message)
2839 if footer_change_ids == [change_id]:
2840 break
2841 if not footer_change_ids:
2842 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002843 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002844 continue
2845 # There is already a valid footer but with different or several ids.
2846 # Doing this automatically is non-trivial as we don't want to lose
2847 # existing other footers, yet we want to append just 1 desired
2848 # Change-Id. Thus, just create a new footer, but let user verify the
2849 # new description.
2850 message = '%s\n\nChange-Id: %s' % (message, change_id)
2851 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002852 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002853 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002854 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002855 'Please, check the proposed correction to the description, '
2856 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2857 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2858 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002859 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002860 if not options.force:
2861 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002862 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002863 message = change_desc.description
2864 if not message:
2865 DieWithError("Description is empty. Aborting...")
2866 # Continue the while loop.
2867 # Sanity check of this code - we should end up with proper message
2868 # footer.
2869 assert [change_id] == git_footers.get_footer_change_id(message)
2870 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002871 else: # if not self.GetIssue()
2872 if options.message:
2873 message = options.message
2874 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002875 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002876 if options.title:
2877 message = options.title + '\n\n' + message
2878 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002879
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002880 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002881 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002882 # On first upload, patchset title is always this string, while
2883 # --title flag gets converted to first line of message.
2884 title = 'Initial upload'
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002885 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002886 if not change_desc.description:
2887 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002888 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002889 if len(change_ids) > 1:
2890 DieWithError('too many Change-Id footers, at most 1 allowed.')
2891 if not change_ids:
2892 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002893 change_desc.set_description(git_footers.add_footer_change_id(
2894 change_desc.description,
2895 GenerateGerritChangeId(change_desc.description)))
2896 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002897 assert len(change_ids) == 1
2898 change_id = change_ids[0]
2899
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002900 if options.reviewers or options.tbrs or options.add_owners_to:
2901 change_desc.update_reviewers(options.reviewers, options.tbrs,
2902 options.add_owners_to, change)
2903
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002904 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002905 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2906 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002907 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2908 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002909 '-m', change_desc.description]).strip()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002910 else:
2911 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002912 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002913 if not change_desc.description:
2914 DieWithError("Description is empty. Aborting...")
2915
2916 if not git_footers.get_footer_change_id(change_desc.description):
2917 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002918 change_desc.set_description(
2919 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002920 if options.reviewers or options.tbrs or options.add_owners_to:
2921 change_desc.update_reviewers(options.reviewers, options.tbrs,
2922 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002923 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002924 # For no-squash mode, we assume the remote called "origin" is the one we
2925 # want. It is not worthwhile to support different workflows for
2926 # no-squash mode.
2927 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002928 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2929
2930 assert change_desc
2931 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2932 ref_to_push)]).splitlines()
2933 if len(commits) > 1:
2934 print('WARNING: This will upload %d commits. Run the following command '
2935 'to see which commits will be uploaded: ' % len(commits))
2936 print('git log %s..%s' % (parent, ref_to_push))
2937 print('You can also use `git squash-branch` to squash these into a '
2938 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002939 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002940
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002941 if options.reviewers or options.tbrs or options.add_owners_to:
2942 change_desc.update_reviewers(options.reviewers, options.tbrs,
2943 options.add_owners_to, change)
2944
2945 if options.send_mail:
2946 if not change_desc.get_reviewers():
2947 DieWithError('Must specify reviewers to send email.', change_desc)
2948
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002949 # Extra options that can be specified at push time. Doc:
2950 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002951 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002952 if change_desc.get_reviewers(tbr_only=True):
2953 print('Adding self-LGTM (Code-Review +1) because of TBRs')
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002954 refspec_opts.append('l=Code-Review+1')
tandrii99a72f22016-08-17 14:33:24 -07002955
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002956
2957 # TODO(tandrii): options.message should be posted as a comment
2958 # if --send-email is set on non-initial upload as Rietveld used to do it.
2959
Aaron Gable9b713dd2016-12-14 16:04:21 -08002960 if title:
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002961 if not re.match(r'^[\w ]+$', title):
2962 title = re.sub(r'[^\w ]', '', title)
2963 if not automatic_title:
2964 print('WARNING: Patchset title may only contain alphanumeric chars '
2965 'and spaces. You can edit it in the UI. '
2966 'See https://crbug.com/663787.\n'
2967 'Cleaned up title: %s' % title)
2968 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2969 # reverse on its side.
2970 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002971
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002972 # Never notify now because no one is on the review. Notify when we add
2973 # reviewers and CCs below.
2974 refspec_opts.append('notify=NONE')
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002975
agablec6787972016-09-09 16:13:34 -07002976 if options.private:
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002977 refspec_opts.append('draft')
agablec6787972016-09-09 16:13:34 -07002978
rmistry9eadede2016-09-19 11:22:43 -07002979 if options.topic:
2980 # Documentation on Gerrit topics is here:
2981 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002982 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002983
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002984 refspec_suffix = ''
2985 if refspec_opts:
2986 refspec_suffix = '%' + ','.join(refspec_opts)
2987 assert ' ' not in refspec_suffix, (
2988 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2989 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2990
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002991 try:
2992 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002993 ['git', 'push', self.GetRemoteUrl(), refspec],
2994 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002995 # Flush after every line: useful for seeing progress when running as
2996 # recipe.
2997 filter_fn=lambda _: sys.stdout.flush())
2998 except subprocess2.CalledProcessError:
2999 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003000 'for the reason of the failure.\n'
3001 'Hint: run command below to diangose common Git/Gerrit '
3002 'credential problems:\n'
3003 ' git cl creds-check\n',
3004 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003005
3006 if options.squash:
3007 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
3008 change_numbers = [m.group(1)
3009 for m in map(regex.match, push_stdout.splitlines())
3010 if m]
3011 if len(change_numbers) != 1:
3012 DieWithError(
3013 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003014 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003015 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003016 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003017
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003018 reviewers = sorted(change_desc.get_reviewers())
3019
tandrii88189772016-09-29 04:29:57 -07003020 # Add cc's from the CC_LIST and --cc flag (if any).
3021 cc = self.GetCCList().split(',')
3022 if options.cc:
3023 cc.extend(options.cc)
3024 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003025 if change_desc.get_cced():
3026 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003027
3028 gerrit_util.AddReviewers(
3029 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3030 notify=bool(options.send_mail))
3031
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003032 return 0
3033
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003034 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3035 change_desc):
3036 """Computes parent of the generated commit to be uploaded to Gerrit.
3037
3038 Returns revision or a ref name.
3039 """
3040 if custom_cl_base:
3041 # Try to avoid creating additional unintended CLs when uploading, unless
3042 # user wants to take this risk.
3043 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3044 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3045 local_ref_of_target_remote])
3046 if code == 1:
3047 print('\nWARNING: manually specified base of this CL `%s` '
3048 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3049 'If you proceed with upload, more than 1 CL may be created by '
3050 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3051 'If you are certain that specified base `%s` has already been '
3052 'uploaded to Gerrit as another CL, you may proceed.\n' %
3053 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3054 if not force:
3055 confirm_or_exit(
3056 'Do you take responsibility for cleaning up potential mess '
3057 'resulting from proceeding with upload?',
3058 action='upload')
3059 return custom_cl_base
3060
Aaron Gablef97e33d2017-03-30 15:44:27 -07003061 if remote != '.':
3062 return self.GetCommonAncestorWithUpstream()
3063
3064 # If our upstream branch is local, we base our squashed commit on its
3065 # squashed version.
3066 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3067
Aaron Gablef97e33d2017-03-30 15:44:27 -07003068 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003069 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003070
3071 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003072 # TODO(tandrii): consider checking parent change in Gerrit and using its
3073 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3074 # the tree hash of the parent branch. The upside is less likely bogus
3075 # requests to reupload parent change just because it's uploadhash is
3076 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003077 parent = RunGit(['config',
3078 'branch.%s.gerritsquashhash' % upstream_branch_name],
3079 error_ok=True).strip()
3080 # Verify that the upstream branch has been uploaded too, otherwise
3081 # Gerrit will create additional CLs when uploading.
3082 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3083 RunGitSilent(['rev-parse', parent + ':'])):
3084 DieWithError(
3085 '\nUpload upstream branch %s first.\n'
3086 'It is likely that this branch has been rebased since its last '
3087 'upload, so you just need to upload it again.\n'
3088 '(If you uploaded it with --no-squash, then branch dependencies '
3089 'are not supported, and you should reupload with --squash.)'
3090 % upstream_branch_name,
3091 change_desc)
3092 return parent
3093
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003094 def _AddChangeIdToCommitMessage(self, options, args):
3095 """Re-commits using the current message, assumes the commit hook is in
3096 place.
3097 """
3098 log_desc = options.message or CreateDescriptionFromLog(args)
3099 git_command = ['commit', '--amend', '-m', log_desc]
3100 RunGit(git_command)
3101 new_log_desc = CreateDescriptionFromLog(args)
3102 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003103 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003104 return new_log_desc
3105 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003106 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003107
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003108 def SetCQState(self, new_state):
3109 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003110 vote_map = {
3111 _CQState.NONE: 0,
3112 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003113 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003114 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01003115 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
3116 if new_state == _CQState.DRY_RUN:
3117 # Don't spam everybody reviewer/owner.
3118 kwargs['notify'] = 'NONE'
3119 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003120
tandriie113dfd2016-10-11 10:20:12 -07003121 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003122 try:
3123 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003124 except GerritChangeNotExists:
3125 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003126
3127 if data['status'] in ('ABANDONED', 'MERGED'):
3128 return 'CL %s is closed' % self.GetIssue()
3129
3130 def GetTryjobProperties(self, patchset=None):
3131 """Returns dictionary of properties to launch tryjob."""
3132 data = self._GetChangeDetail(['ALL_REVISIONS'])
3133 patchset = int(patchset or self.GetPatchset())
3134 assert patchset
3135 revision_data = None # Pylint wants it to be defined.
3136 for revision_data in data['revisions'].itervalues():
3137 if int(revision_data['_number']) == patchset:
3138 break
3139 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003140 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003141 (patchset, self.GetIssue()))
3142 return {
3143 'patch_issue': self.GetIssue(),
3144 'patch_set': patchset or self.GetPatchset(),
3145 'patch_project': data['project'],
3146 'patch_storage': 'gerrit',
3147 'patch_ref': revision_data['fetch']['http']['ref'],
3148 'patch_repository_url': revision_data['fetch']['http']['url'],
3149 'patch_gerrit_url': self.GetCodereviewServer(),
3150 }
tandriie113dfd2016-10-11 10:20:12 -07003151
tandriide281ae2016-10-12 06:02:30 -07003152 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003153 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003154
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003155
3156_CODEREVIEW_IMPLEMENTATIONS = {
3157 'rietveld': _RietveldChangelistImpl,
3158 'gerrit': _GerritChangelistImpl,
3159}
3160
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003161
iannuccie53c9352016-08-17 14:40:40 -07003162def _add_codereview_issue_select_options(parser, extra=""):
3163 _add_codereview_select_options(parser)
3164
3165 text = ('Operate on this issue number instead of the current branch\'s '
3166 'implicit issue.')
3167 if extra:
3168 text += ' '+extra
3169 parser.add_option('-i', '--issue', type=int, help=text)
3170
3171
3172def _process_codereview_issue_select_options(parser, options):
3173 _process_codereview_select_options(parser, options)
3174 if options.issue is not None and not options.forced_codereview:
3175 parser.error('--issue must be specified with either --rietveld or --gerrit')
3176
3177
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003178def _add_codereview_select_options(parser):
3179 """Appends --gerrit and --rietveld options to force specific codereview."""
3180 parser.codereview_group = optparse.OptionGroup(
3181 parser, 'EXPERIMENTAL! Codereview override options')
3182 parser.add_option_group(parser.codereview_group)
3183 parser.codereview_group.add_option(
3184 '--gerrit', action='store_true',
3185 help='Force the use of Gerrit for codereview')
3186 parser.codereview_group.add_option(
3187 '--rietveld', action='store_true',
3188 help='Force the use of Rietveld for codereview')
3189
3190
3191def _process_codereview_select_options(parser, options):
3192 if options.gerrit and options.rietveld:
3193 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3194 options.forced_codereview = None
3195 if options.gerrit:
3196 options.forced_codereview = 'gerrit'
3197 elif options.rietveld:
3198 options.forced_codereview = 'rietveld'
3199
3200
tandriif9aefb72016-07-01 09:06:51 -07003201def _get_bug_line_values(default_project, bugs):
3202 """Given default_project and comma separated list of bugs, yields bug line
3203 values.
3204
3205 Each bug can be either:
3206 * a number, which is combined with default_project
3207 * string, which is left as is.
3208
3209 This function may produce more than one line, because bugdroid expects one
3210 project per line.
3211
3212 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3213 ['v8:123', 'chromium:789']
3214 """
3215 default_bugs = []
3216 others = []
3217 for bug in bugs.split(','):
3218 bug = bug.strip()
3219 if bug:
3220 try:
3221 default_bugs.append(int(bug))
3222 except ValueError:
3223 others.append(bug)
3224
3225 if default_bugs:
3226 default_bugs = ','.join(map(str, default_bugs))
3227 if default_project:
3228 yield '%s:%s' % (default_project, default_bugs)
3229 else:
3230 yield default_bugs
3231 for other in sorted(others):
3232 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3233 yield other
3234
3235
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003236class ChangeDescription(object):
3237 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003238 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003239 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003240 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003241 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003242
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003243 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003244 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003245
agable@chromium.org42c20792013-09-12 17:34:49 +00003246 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003247 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003248 return '\n'.join(self._description_lines)
3249
3250 def set_description(self, desc):
3251 if isinstance(desc, basestring):
3252 lines = desc.splitlines()
3253 else:
3254 lines = [line.rstrip() for line in desc]
3255 while lines and not lines[0]:
3256 lines.pop(0)
3257 while lines and not lines[-1]:
3258 lines.pop(-1)
3259 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003260
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003261 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3262 """Rewrites the R=/TBR= line(s) as a single line each.
3263
3264 Args:
3265 reviewers (list(str)) - list of additional emails to use for reviewers.
3266 tbrs (list(str)) - list of additional emails to use for TBRs.
3267 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3268 the change that are missing OWNER coverage. If this is not None, you
3269 must also pass a value for `change`.
3270 change (Change) - The Change that should be used for OWNERS lookups.
3271 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003272 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003273 assert isinstance(tbrs, list), tbrs
3274
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003275 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003276 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003277
3278 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003279 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003280
3281 reviewers = set(reviewers)
3282 tbrs = set(tbrs)
3283 LOOKUP = {
3284 'TBR': tbrs,
3285 'R': reviewers,
3286 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003287
agable@chromium.org42c20792013-09-12 17:34:49 +00003288 # Get the set of R= and TBR= lines and remove them from the desciption.
3289 regexp = re.compile(self.R_LINE)
3290 matches = [regexp.match(line) for line in self._description_lines]
3291 new_desc = [l for i, l in enumerate(self._description_lines)
3292 if not matches[i]]
3293 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003294
agable@chromium.org42c20792013-09-12 17:34:49 +00003295 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003296
3297 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003298 for match in matches:
3299 if not match:
3300 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003301 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3302
3303 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003304 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003305 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003306 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003307 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003308 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003309 LOOKUP[add_owners_to].update(
3310 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003311
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003312 # If any folks ended up in both groups, remove them from tbrs.
3313 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003314
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003315 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3316 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003317
3318 # Put the new lines in the description where the old first R= line was.
3319 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3320 if 0 <= line_loc < len(self._description_lines):
3321 if new_tbr_line:
3322 self._description_lines.insert(line_loc, new_tbr_line)
3323 if new_r_line:
3324 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003325 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003326 if new_r_line:
3327 self.append_footer(new_r_line)
3328 if new_tbr_line:
3329 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003330
Aaron Gable3a16ed12017-03-23 10:51:55 -07003331 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003332 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003333 self.set_description([
3334 '# Enter a description of the change.',
3335 '# This will be displayed on the codereview site.',
3336 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003337 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003338 '--------------------',
3339 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003340
agable@chromium.org42c20792013-09-12 17:34:49 +00003341 regexp = re.compile(self.BUG_LINE)
3342 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003343 prefix = settings.GetBugPrefix()
3344 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003345 if git_footer:
3346 self.append_footer('Bug: %s' % ', '.join(values))
3347 else:
3348 for value in values:
3349 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003350
agable@chromium.org42c20792013-09-12 17:34:49 +00003351 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003352 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003353 if not content:
3354 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003355 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003356
3357 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003358 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3359 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003360 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003361 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003362
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003363 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003364 """Adds a footer line to the description.
3365
3366 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3367 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3368 that Gerrit footers are always at the end.
3369 """
3370 parsed_footer_line = git_footers.parse_footer(line)
3371 if parsed_footer_line:
3372 # Line is a gerrit footer in the form: Footer-Key: any value.
3373 # Thus, must be appended observing Gerrit footer rules.
3374 self.set_description(
3375 git_footers.add_footer(self.description,
3376 key=parsed_footer_line[0],
3377 value=parsed_footer_line[1]))
3378 return
3379
3380 if not self._description_lines:
3381 self._description_lines.append(line)
3382 return
3383
3384 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3385 if gerrit_footers:
3386 # git_footers.split_footers ensures that there is an empty line before
3387 # actual (gerrit) footers, if any. We have to keep it that way.
3388 assert top_lines and top_lines[-1] == ''
3389 top_lines, separator = top_lines[:-1], top_lines[-1:]
3390 else:
3391 separator = [] # No need for separator if there are no gerrit_footers.
3392
3393 prev_line = top_lines[-1] if top_lines else ''
3394 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3395 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3396 top_lines.append('')
3397 top_lines.append(line)
3398 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003399
tandrii99a72f22016-08-17 14:33:24 -07003400 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003401 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003402 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003403 reviewers = [match.group(2).strip()
3404 for match in matches
3405 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003406 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003407
bradnelsond975b302016-10-23 12:20:23 -07003408 def get_cced(self):
3409 """Retrieves the list of reviewers."""
3410 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3411 cced = [match.group(2).strip() for match in matches if match]
3412 return cleanup_list(cced)
3413
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003414 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3415 """Updates this commit description given the parent.
3416
3417 This is essentially what Gnumbd used to do.
3418 Consult https://goo.gl/WMmpDe for more details.
3419 """
3420 assert parent_msg # No, orphan branch creation isn't supported.
3421 assert parent_hash
3422 assert dest_ref
3423 parent_footer_map = git_footers.parse_footers(parent_msg)
3424 # This will also happily parse svn-position, which GnumbD is no longer
3425 # supporting. While we'd generate correct footers, the verifier plugin
3426 # installed in Gerrit will block such commit (ie git push below will fail).
3427 parent_position = git_footers.get_position(parent_footer_map)
3428
3429 # Cherry-picks may have last line obscuring their prior footers,
3430 # from git_footers perspective. This is also what Gnumbd did.
3431 cp_line = None
3432 if (self._description_lines and
3433 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3434 cp_line = self._description_lines.pop()
3435
3436 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3437
3438 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3439 # user interference with actual footers we'd insert below.
3440 for i, (k, v) in enumerate(parsed_footers):
3441 if k.startswith('Cr-'):
3442 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3443
3444 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003445 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003446 if parent_position[0] == dest_ref:
3447 # Same branch as parent.
3448 number = int(parent_position[1]) + 1
3449 else:
3450 number = 1 # New branch, and extra lineage.
3451 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3452 int(parent_position[1])))
3453
3454 parsed_footers.append(('Cr-Commit-Position',
3455 '%s@{#%d}' % (dest_ref, number)))
3456 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3457
3458 self._description_lines = top_lines
3459 if cp_line:
3460 self._description_lines.append(cp_line)
3461 if self._description_lines[-1] != '':
3462 self._description_lines.append('') # Ensure footer separator.
3463 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3464
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003465
Aaron Gablea1bab272017-04-11 16:38:18 -07003466def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003467 """Retrieves the reviewers that approved a CL from the issue properties with
3468 messages.
3469
3470 Note that the list may contain reviewers that are not committer, thus are not
3471 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003472
3473 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003474 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003475 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003476 return sorted(
3477 set(
3478 message['sender']
3479 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003480 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003481 )
3482 )
3483
3484
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003485def FindCodereviewSettingsFile(filename='codereview.settings'):
3486 """Finds the given file starting in the cwd and going up.
3487
3488 Only looks up to the top of the repository unless an
3489 'inherit-review-settings-ok' file exists in the root of the repository.
3490 """
3491 inherit_ok_file = 'inherit-review-settings-ok'
3492 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003493 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003494 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3495 root = '/'
3496 while True:
3497 if filename in os.listdir(cwd):
3498 if os.path.isfile(os.path.join(cwd, filename)):
3499 return open(os.path.join(cwd, filename))
3500 if cwd == root:
3501 break
3502 cwd = os.path.dirname(cwd)
3503
3504
3505def LoadCodereviewSettingsFromFile(fileobj):
3506 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003507 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003508
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003509 def SetProperty(name, setting, unset_error_ok=False):
3510 fullname = 'rietveld.' + name
3511 if setting in keyvals:
3512 RunGit(['config', fullname, keyvals[setting]])
3513 else:
3514 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3515
tandrii48df5812016-10-17 03:55:37 -07003516 if not keyvals.get('GERRIT_HOST', False):
3517 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003518 # Only server setting is required. Other settings can be absent.
3519 # In that case, we ignore errors raised during option deletion attempt.
3520 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003521 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003522 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3523 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003524 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003525 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3526 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003527 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003528 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3529 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003530
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003531 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003532 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003533
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003534 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003535 RunGit(['config', 'gerrit.squash-uploads',
3536 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003537
tandrii@chromium.org28253532016-04-14 13:46:56 +00003538 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003539 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003540 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3541
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003542 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003543 # should be of the form
3544 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3545 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003546 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3547 keyvals['ORIGIN_URL_CONFIG']])
3548
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003549
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003550def urlretrieve(source, destination):
3551 """urllib is broken for SSL connections via a proxy therefore we
3552 can't use urllib.urlretrieve()."""
3553 with open(destination, 'w') as f:
3554 f.write(urllib2.urlopen(source).read())
3555
3556
ukai@chromium.org712d6102013-11-27 00:52:58 +00003557def hasSheBang(fname):
3558 """Checks fname is a #! script."""
3559 with open(fname) as f:
3560 return f.read(2).startswith('#!')
3561
3562
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003563# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3564def DownloadHooks(*args, **kwargs):
3565 pass
3566
3567
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003568def DownloadGerritHook(force):
3569 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003570
3571 Args:
3572 force: True to update hooks. False to install hooks if not present.
3573 """
3574 if not settings.GetIsGerrit():
3575 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003576 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003577 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3578 if not os.access(dst, os.X_OK):
3579 if os.path.exists(dst):
3580 if not force:
3581 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003582 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003583 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003584 if not hasSheBang(dst):
3585 DieWithError('Not a script: %s\n'
3586 'You need to download from\n%s\n'
3587 'into .git/hooks/commit-msg and '
3588 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003589 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3590 except Exception:
3591 if os.path.exists(dst):
3592 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003593 DieWithError('\nFailed to download hooks.\n'
3594 'You need to download from\n%s\n'
3595 'into .git/hooks/commit-msg and '
3596 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003597
3598
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003599def GetRietveldCodereviewSettingsInteractively():
3600 """Prompt the user for settings."""
3601 server = settings.GetDefaultServerUrl(error_ok=True)
3602 prompt = 'Rietveld server (host[:port])'
3603 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3604 newserver = ask_for_data(prompt + ':')
3605 if not server and not newserver:
3606 newserver = DEFAULT_SERVER
3607 if newserver:
3608 newserver = gclient_utils.UpgradeToHttps(newserver)
3609 if newserver != server:
3610 RunGit(['config', 'rietveld.server', newserver])
3611
3612 def SetProperty(initial, caption, name, is_url):
3613 prompt = caption
3614 if initial:
3615 prompt += ' ("x" to clear) [%s]' % initial
3616 new_val = ask_for_data(prompt + ':')
3617 if new_val == 'x':
3618 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3619 elif new_val:
3620 if is_url:
3621 new_val = gclient_utils.UpgradeToHttps(new_val)
3622 if new_val != initial:
3623 RunGit(['config', 'rietveld.' + name, new_val])
3624
3625 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3626 SetProperty(settings.GetDefaultPrivateFlag(),
3627 'Private flag (rietveld only)', 'private', False)
3628 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3629 'tree-status-url', False)
3630 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3631 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3632 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3633 'run-post-upload-hook', False)
3634
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003635
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003636class _GitCookiesChecker(object):
3637 """Provides facilties for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003638
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003639 _GOOGLESOURCE = 'googlesource.com'
3640
3641 def __init__(self):
3642 # Cached list of [host, identity, source], where source is either
3643 # .gitcookies or .netrc.
3644 self._all_hosts = None
3645
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003646 def ensure_configured_gitcookies(self):
3647 """Runs checks and suggests fixes to make git use .gitcookies from default
3648 path."""
3649 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3650 configured_path = RunGitSilent(
3651 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003652 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003653 if configured_path:
3654 self._ensure_default_gitcookies_path(configured_path, default)
3655 else:
3656 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003657
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003658 @staticmethod
3659 def _ensure_default_gitcookies_path(configured_path, default_path):
3660 assert configured_path
3661 if configured_path == default_path:
3662 print('git is already configured to use your .gitcookies from %s' %
3663 configured_path)
3664 return
3665
3666 print('WARNING: you have configured custom path to .gitcookies: %s\n'
3667 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3668 (configured_path, default_path))
3669
3670 if not os.path.exists(configured_path):
3671 print('However, your configured .gitcookies file is missing.')
3672 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3673 action='reconfigure')
3674 RunGit(['config', '--global', 'http.cookiefile', default_path])
3675 return
3676
3677 if os.path.exists(default_path):
3678 print('WARNING: default .gitcookies file already exists %s' %
3679 default_path)
3680 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3681 default_path)
3682
3683 confirm_or_exit('Move existing .gitcookies to default location?',
3684 action='move')
3685 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003686 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003687 print('Moved and reconfigured git to use .gitcookies from %s' %
3688 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003689
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003690 @staticmethod
3691 def _configure_gitcookies_path(default_path):
3692 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3693 if os.path.exists(netrc_path):
3694 print('You seem to be using outdated .netrc for git credentials: %s' %
3695 netrc_path)
3696 print('This tool will guide you through setting up recommended '
3697 '.gitcookies store for git credentials.\n'
3698 '\n'
3699 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3700 ' git config --global --unset http.cookiefile\n'
3701 ' mv %s %s.backup\n\n' % (default_path, default_path))
3702 confirm_or_exit(action='setup .gitcookies')
3703 RunGit(['config', '--global', 'http.cookiefile', default_path])
3704 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003705
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003706 def get_hosts_with_creds(self, include_netrc=False):
3707 if self._all_hosts is None:
3708 a = gerrit_util.CookiesAuthenticator()
3709 self._all_hosts = [
3710 (h, u, s)
3711 for h, u, s in itertools.chain(
3712 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3713 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3714 )
3715 if h.endswith(self._GOOGLESOURCE)
3716 ]
3717
3718 if include_netrc:
3719 return self._all_hosts
3720 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3721
3722 def print_current_creds(self, include_netrc=False):
3723 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3724 if not hosts:
3725 print('No Git/Gerrit credentials found')
3726 return
3727 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3728 header = [('Host', 'User', 'Which file'),
3729 ['=' * l for l in lengths]]
3730 for row in (header + hosts):
3731 print('\t'.join((('%%+%ds' % l) % s)
3732 for l, s in zip(lengths, row)))
3733
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003734 @staticmethod
3735 def _parse_identity(identity):
3736 """Parses identity "git-<ldap>.example.com" into <ldap> and domain."""
3737 username, domain = identity.split('.', 1)
3738 if username.startswith('git-'):
3739 username = username[len('git-'):]
3740 return username, domain
3741
3742 def _get_usernames_of_domain(self, domain):
3743 """Returns list of usernames referenced by .gitcookies in a given domain."""
3744 identities_by_domain = {}
3745 for _, identity, _ in self.get_hosts_with_creds():
3746 username, domain = self._parse_identity(identity)
3747 identities_by_domain.setdefault(domain, []).append(username)
3748 return identities_by_domain.get(domain)
3749
3750 def _canonical_git_googlesource_host(self, host):
3751 """Normalizes Gerrit hosts (with '-review') to Git host."""
3752 assert host.endswith(self._GOOGLESOURCE)
3753 # Prefix doesn't include '.' at the end.
3754 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3755 if prefix.endswith('-review'):
3756 prefix = prefix[:-len('-review')]
3757 return prefix + '.' + self._GOOGLESOURCE
3758
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003759 def _canonical_gerrit_googlesource_host(self, host):
3760 git_host = self._canonical_git_googlesource_host(host)
3761 prefix = git_host.split('.', 1)[0]
3762 return prefix + '-review.' + self._GOOGLESOURCE
3763
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003764 def has_generic_host(self):
3765 """Returns whether generic .googlesource.com has been configured.
3766
3767 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3768 """
3769 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3770 if host == '.' + self._GOOGLESOURCE:
3771 return True
3772 return False
3773
3774 def _get_git_gerrit_identity_pairs(self):
3775 """Returns map from canonic host to pair of identities (Git, Gerrit).
3776
3777 One of identities might be None, meaning not configured.
3778 """
3779 host_to_identity_pairs = {}
3780 for host, identity, _ in self.get_hosts_with_creds():
3781 canonical = self._canonical_git_googlesource_host(host)
3782 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3783 idx = 0 if canonical == host else 1
3784 pair[idx] = identity
3785 return host_to_identity_pairs
3786
3787 def get_partially_configured_hosts(self):
3788 return set(
3789 host for host, identities_pair in
3790 self._get_git_gerrit_identity_pairs().iteritems()
3791 if None in identities_pair and host != '.' + self._GOOGLESOURCE)
3792
3793 def get_conflicting_hosts(self):
3794 return set(
3795 host for host, (i1, i2) in
3796 self._get_git_gerrit_identity_pairs().iteritems()
3797 if None not in (i1, i2) and i1 != i2)
3798
3799 def get_duplicated_hosts(self):
3800 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3801 return set(host for host, count in counters.iteritems() if count > 1)
3802
3803 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3804 'chromium.googlesource.com': 'chromium.org',
3805 'chrome-internal.googlesource.com': 'google.com',
3806 }
3807
3808 def get_hosts_with_wrong_identities(self):
3809 """Finds hosts which **likely** reference wrong identities.
3810
3811 Note: skips hosts which have conflicting identities for Git and Gerrit.
3812 """
3813 hosts = set()
3814 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3815 pair = self._get_git_gerrit_identity_pairs().get(host)
3816 if pair and pair[0] == pair[1]:
3817 _, domain = self._parse_identity(pair[0])
3818 if domain != expected:
3819 hosts.add(host)
3820 return hosts
3821
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003822 @staticmethod
3823 def print_hosts(hosts, extra_column_func=None):
3824 hosts = sorted(hosts)
3825 assert hosts
3826 if extra_column_func is None:
3827 extras = [''] * len(hosts)
3828 else:
3829 extras = [extra_column_func(host) for host in hosts]
3830 tmpl = ' %%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3831 for he in zip(hosts, extras):
3832 print(tmpl % he)
3833 print()
3834
3835 def find_and_report_problems(self):
3836 """Returns True if there was at least one problem, else False."""
3837 problems = [False]
3838 def add_problem():
3839 if not problems[0]:
Andrii Shyshkalov4812e612017-03-27 17:22:57 +02003840 print('\n\n.gitcookies problem report:\n')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003841 problems[0] = True
3842
3843 if self.has_generic_host():
3844 add_problem()
3845 print(' .googlesource.com record detected\n'
3846 ' Chrome Infrastructure team recommends to list full host names '
3847 'explicitly.\n')
3848
3849 dups = self.get_duplicated_hosts()
3850 if dups:
3851 add_problem()
3852 print(' The following hosts were defined twice:\n')
3853 self.print_hosts(dups)
3854
3855 partial = self.get_partially_configured_hosts()
3856 if partial:
3857 add_problem()
3858 print(' Credentials should come in pairs for Git and Gerrit hosts. '
3859 'These hosts are missing:')
3860 self.print_hosts(partial)
3861
3862 conflicting = self.get_conflicting_hosts()
3863 if conflicting:
3864 add_problem()
3865 print(' The following Git hosts have differing credentials from their '
3866 'Gerrit counterparts:\n')
3867 self.print_hosts(conflicting, lambda host: '%s vs %s' %
3868 tuple(self._get_git_gerrit_identity_pairs()[host]))
3869
3870 wrong = self.get_hosts_with_wrong_identities()
3871 if wrong:
3872 add_problem()
3873 print(' These hosts likely use wrong identity:\n')
3874 self.print_hosts(wrong, lambda host: '%s but %s recommended' %
3875 (self._get_git_gerrit_identity_pairs()[host][0],
3876 self._EXPECTED_HOST_IDENTITY_DOMAINS[host]))
3877 return problems[0]
3878
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003879
3880def CMDcreds_check(parser, args):
3881 """Checks credentials and suggests changes."""
3882 _, _ = parser.parse_args(args)
3883
3884 if gerrit_util.GceAuthenticator.is_gce():
3885 DieWithError('this command is not designed for GCE, are you on a bot?')
3886
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003887 checker = _GitCookiesChecker()
3888 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003889
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003890 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003891 checker.print_current_creds(include_netrc=True)
3892
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003893 if not checker.find_and_report_problems():
3894 print('\nNo problems detected in your .gitcookies')
3895 return 0
3896 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003897
3898
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003899@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003900def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003901 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003902
tandrii5d0a0422016-09-14 06:24:35 -07003903 print('WARNING: git cl config works for Rietveld only')
3904 # TODO(tandrii): remove this once we switch to Gerrit.
3905 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003906 parser.add_option('--activate-update', action='store_true',
3907 help='activate auto-updating [rietveld] section in '
3908 '.git/config')
3909 parser.add_option('--deactivate-update', action='store_true',
3910 help='deactivate auto-updating [rietveld] section in '
3911 '.git/config')
3912 options, args = parser.parse_args(args)
3913
3914 if options.deactivate_update:
3915 RunGit(['config', 'rietveld.autoupdate', 'false'])
3916 return
3917
3918 if options.activate_update:
3919 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3920 return
3921
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003922 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003923 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003924 return 0
3925
3926 url = args[0]
3927 if not url.endswith('codereview.settings'):
3928 url = os.path.join(url, 'codereview.settings')
3929
3930 # Load code review settings and download hooks (if available).
3931 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3932 return 0
3933
3934
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003935def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003936 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003937 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3938 branch = ShortBranchName(branchref)
3939 _, args = parser.parse_args(args)
3940 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003941 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003942 return RunGit(['config', 'branch.%s.base-url' % branch],
3943 error_ok=False).strip()
3944 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003945 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003946 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3947 error_ok=False).strip()
3948
3949
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003950def color_for_status(status):
3951 """Maps a Changelist status to color, for CMDstatus and other tools."""
3952 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003953 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003954 'waiting': Fore.BLUE,
3955 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003956 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003957 'lgtm': Fore.GREEN,
3958 'commit': Fore.MAGENTA,
3959 'closed': Fore.CYAN,
3960 'error': Fore.WHITE,
3961 }.get(status, Fore.WHITE)
3962
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003963
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003964def get_cl_statuses(changes, fine_grained, max_processes=None):
3965 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003966
3967 If fine_grained is true, this will fetch CL statuses from the server.
3968 Otherwise, simply indicate if there's a matching url for the given branches.
3969
3970 If max_processes is specified, it is used as the maximum number of processes
3971 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3972 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003973
3974 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003975 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003976 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003977 upload.verbosity = 0
3978
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003979 if not changes:
3980 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003981
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003982 if not fine_grained:
3983 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003984 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003985 for cl in changes:
3986 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003987 return
3988
3989 # First, sort out authentication issues.
3990 logging.debug('ensuring credentials exist')
3991 for cl in changes:
3992 cl.EnsureAuthenticated(force=False, refresh=True)
3993
3994 def fetch(cl):
3995 try:
3996 return (cl, cl.GetStatus())
3997 except:
3998 # See http://crbug.com/629863.
3999 logging.exception('failed to fetch status for %s:', cl)
4000 raise
4001
4002 threads_count = len(changes)
4003 if max_processes:
4004 threads_count = max(1, min(threads_count, max_processes))
4005 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4006
4007 pool = ThreadPool(threads_count)
4008 fetched_cls = set()
4009 try:
4010 it = pool.imap_unordered(fetch, changes).__iter__()
4011 while True:
4012 try:
4013 cl, status = it.next(timeout=5)
4014 except multiprocessing.TimeoutError:
4015 break
4016 fetched_cls.add(cl)
4017 yield cl, status
4018 finally:
4019 pool.close()
4020
4021 # Add any branches that failed to fetch.
4022 for cl in set(changes) - fetched_cls:
4023 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004024
rmistry@google.com2dd99862015-06-22 12:22:18 +00004025
4026def upload_branch_deps(cl, args):
4027 """Uploads CLs of local branches that are dependents of the current branch.
4028
4029 If the local branch dependency tree looks like:
4030 test1 -> test2.1 -> test3.1
4031 -> test3.2
4032 -> test2.2 -> test3.3
4033
4034 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4035 run on the dependent branches in this order:
4036 test2.1, test3.1, test3.2, test2.2, test3.3
4037
4038 Note: This function does not rebase your local dependent branches. Use it when
4039 you make a change to the parent branch that will not conflict with its
4040 dependent branches, and you would like their dependencies updated in
4041 Rietveld.
4042 """
4043 if git_common.is_dirty_git_tree('upload-branch-deps'):
4044 return 1
4045
4046 root_branch = cl.GetBranch()
4047 if root_branch is None:
4048 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4049 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004050 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004051 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4052 'patchset dependencies without an uploaded CL.')
4053
4054 branches = RunGit(['for-each-ref',
4055 '--format=%(refname:short) %(upstream:short)',
4056 'refs/heads'])
4057 if not branches:
4058 print('No local branches found.')
4059 return 0
4060
4061 # Create a dictionary of all local branches to the branches that are dependent
4062 # on it.
4063 tracked_to_dependents = collections.defaultdict(list)
4064 for b in branches.splitlines():
4065 tokens = b.split()
4066 if len(tokens) == 2:
4067 branch_name, tracked = tokens
4068 tracked_to_dependents[tracked].append(branch_name)
4069
vapiera7fbd5a2016-06-16 09:17:49 -07004070 print()
4071 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004072 dependents = []
4073 def traverse_dependents_preorder(branch, padding=''):
4074 dependents_to_process = tracked_to_dependents.get(branch, [])
4075 padding += ' '
4076 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004077 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004078 dependents.append(dependent)
4079 traverse_dependents_preorder(dependent, padding)
4080 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004081 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004082
4083 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004084 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004085 return 0
4086
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004087 confirm_or_exit('This command will checkout all dependent branches and run '
4088 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004089
andybons@chromium.org962f9462016-02-03 20:00:42 +00004090 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004091 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004092 args.extend(['-t', 'Updated patchset dependency'])
4093
rmistry@google.com2dd99862015-06-22 12:22:18 +00004094 # Record all dependents that failed to upload.
4095 failures = {}
4096 # Go through all dependents, checkout the branch and upload.
4097 try:
4098 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004099 print()
4100 print('--------------------------------------')
4101 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004102 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004103 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004104 try:
4105 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004106 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004107 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004108 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004109 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004110 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004111 finally:
4112 # Swap back to the original root branch.
4113 RunGit(['checkout', '-q', root_branch])
4114
vapiera7fbd5a2016-06-16 09:17:49 -07004115 print()
4116 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004117 for dependent_branch in dependents:
4118 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004119 print(' %s : %s' % (dependent_branch, upload_status))
4120 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004121
4122 return 0
4123
4124
kmarshall3bff56b2016-06-06 18:31:47 -07004125def CMDarchive(parser, args):
4126 """Archives and deletes branches associated with closed changelists."""
4127 parser.add_option(
4128 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004129 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004130 parser.add_option(
4131 '-f', '--force', action='store_true',
4132 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004133 parser.add_option(
4134 '-d', '--dry-run', action='store_true',
4135 help='Skip the branch tagging and removal steps.')
4136 parser.add_option(
4137 '-t', '--notags', action='store_true',
4138 help='Do not tag archived branches. '
4139 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004140
4141 auth.add_auth_options(parser)
4142 options, args = parser.parse_args(args)
4143 if args:
4144 parser.error('Unsupported args: %s' % ' '.join(args))
4145 auth_config = auth.extract_auth_config_from_options(options)
4146
4147 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4148 if not branches:
4149 return 0
4150
vapiera7fbd5a2016-06-16 09:17:49 -07004151 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004152 changes = [Changelist(branchref=b, auth_config=auth_config)
4153 for b in branches.splitlines()]
4154 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4155 statuses = get_cl_statuses(changes,
4156 fine_grained=True,
4157 max_processes=options.maxjobs)
4158 proposal = [(cl.GetBranch(),
4159 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4160 for cl, status in statuses
4161 if status == 'closed']
4162 proposal.sort()
4163
4164 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004165 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004166 return 0
4167
4168 current_branch = GetCurrentBranch()
4169
vapiera7fbd5a2016-06-16 09:17:49 -07004170 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004171 if options.notags:
4172 for next_item in proposal:
4173 print(' ' + next_item[0])
4174 else:
4175 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4176 for next_item in proposal:
4177 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004178
kmarshall9249e012016-08-23 12:02:16 -07004179 # Quit now on precondition failure or if instructed by the user, either
4180 # via an interactive prompt or by command line flags.
4181 if options.dry_run:
4182 print('\nNo changes were made (dry run).\n')
4183 return 0
4184 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004185 print('You are currently on a branch \'%s\' which is associated with a '
4186 'closed codereview issue, so archive cannot proceed. Please '
4187 'checkout another branch and run this command again.' %
4188 current_branch)
4189 return 1
kmarshall9249e012016-08-23 12:02:16 -07004190 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004191 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4192 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004193 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004194 return 1
4195
4196 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004197 if not options.notags:
4198 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004199 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004200
vapiera7fbd5a2016-06-16 09:17:49 -07004201 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004202
4203 return 0
4204
4205
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004206def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004207 """Show status of changelists.
4208
4209 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004210 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004211 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004212 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004213 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004214 - Magenta in the commit queue
4215 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004216 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004217
4218 Also see 'git cl comments'.
4219 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004220 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004221 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004222 parser.add_option('-f', '--fast', action='store_true',
4223 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004224 parser.add_option(
4225 '-j', '--maxjobs', action='store', type=int,
4226 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004227
4228 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004229 _add_codereview_issue_select_options(
4230 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004231 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004232 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004233 if args:
4234 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004235 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004236
iannuccie53c9352016-08-17 14:40:40 -07004237 if options.issue is not None and not options.field:
4238 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004240 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004241 cl = Changelist(auth_config=auth_config, issue=options.issue,
4242 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004243 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004244 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004245 elif options.field == 'id':
4246 issueid = cl.GetIssue()
4247 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004248 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004249 elif options.field == 'patch':
4250 patchset = cl.GetPatchset()
4251 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004252 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004253 elif options.field == 'status':
4254 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004255 elif options.field == 'url':
4256 url = cl.GetIssueURL()
4257 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004258 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004259 return 0
4260
4261 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4262 if not branches:
4263 print('No local branch found.')
4264 return 0
4265
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004266 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004267 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004268 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004269 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004270 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004271 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004272 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004273
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004274 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004275 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4276 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4277 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004278 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004279 c, status = output.next()
4280 branch_statuses[c.GetBranch()] = status
4281 status = branch_statuses.pop(branch)
4282 url = cl.GetIssueURL()
4283 if url and (not status or status == 'error'):
4284 # The issue probably doesn't exist anymore.
4285 url += ' (broken)'
4286
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004287 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004288 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004289 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004290 color = ''
4291 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004292 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004293 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004294 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004295 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004296
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004297
4298 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004299 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004300 print('Current branch: %s' % branch)
4301 for cl in changes:
4302 if cl.GetBranch() == branch:
4303 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004304 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004305 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004306 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004307 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004308 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004309 print('Issue description:')
4310 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004311 return 0
4312
4313
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004314def colorize_CMDstatus_doc():
4315 """To be called once in main() to add colors to git cl status help."""
4316 colors = [i for i in dir(Fore) if i[0].isupper()]
4317
4318 def colorize_line(line):
4319 for color in colors:
4320 if color in line.upper():
4321 # Extract whitespaces first and the leading '-'.
4322 indent = len(line) - len(line.lstrip(' ')) + 1
4323 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4324 return line
4325
4326 lines = CMDstatus.__doc__.splitlines()
4327 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4328
4329
phajdan.jre328cf92016-08-22 04:12:17 -07004330def write_json(path, contents):
4331 with open(path, 'w') as f:
4332 json.dump(contents, f)
4333
4334
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004335@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004336def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004337 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004338
4339 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004340 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004341 parser.add_option('-r', '--reverse', action='store_true',
4342 help='Lookup the branch(es) for the specified issues. If '
4343 'no issues are specified, all branches with mapped '
4344 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07004345 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004346 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004347 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004348 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004349
dnj@chromium.org406c4402015-03-03 17:22:28 +00004350 if options.reverse:
4351 branches = RunGit(['for-each-ref', 'refs/heads',
4352 '--format=%(refname:short)']).splitlines()
4353
4354 # Reverse issue lookup.
4355 issue_branch_map = {}
4356 for branch in branches:
4357 cl = Changelist(branchref=branch)
4358 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4359 if not args:
4360 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004361 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004362 for issue in args:
4363 if not issue:
4364 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004365 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004366 print('Branch for issue number %s: %s' % (
4367 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004368 if options.json:
4369 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004370 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004371 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004372 if len(args) > 0:
4373 try:
4374 issue = int(args[0])
4375 except ValueError:
4376 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00004377 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00004378 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07004379 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07004380 if options.json:
4381 write_json(options.json, {
4382 'issue': cl.GetIssue(),
4383 'issue_url': cl.GetIssueURL(),
4384 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004385 return 0
4386
4387
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004388def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004389 """Shows or posts review comments for any changelist."""
4390 parser.add_option('-a', '--add-comment', dest='comment',
4391 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004392 parser.add_option('-i', '--issue', dest='issue',
4393 help='review issue id (defaults to current issue). '
4394 'If given, requires --rietveld or --gerrit')
smut@google.comc85ac942015-09-15 16:34:43 +00004395 parser.add_option('-j', '--json-file',
4396 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004397 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004398 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004399 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004400 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004401 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004402
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004403 issue = None
4404 if options.issue:
4405 try:
4406 issue = int(options.issue)
4407 except ValueError:
4408 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004409 if not options.forced_codereview:
4410 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004411
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004412 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004413 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004414 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004415
4416 if options.comment:
4417 cl.AddComment(options.comment)
4418 return 0
4419
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004420 summary = sorted(cl.GetCommentsSummary(), key=lambda c: c.date)
4421 for comment in summary:
4422 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004423 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004424 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004425 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004426 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004427 color = Fore.MAGENTA
4428 else:
4429 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004430 print('\n%s%s %s%s\n%s' % (
4431 color,
4432 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4433 comment.sender,
4434 Fore.RESET,
4435 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4436
smut@google.comc85ac942015-09-15 16:34:43 +00004437 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004438 def pre_serialize(c):
4439 dct = c.__dict__.copy()
4440 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4441 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004442 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004443 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004444 return 0
4445
4446
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004447@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004448def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004449 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004450 parser.add_option('-d', '--display', action='store_true',
4451 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004452 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004453 help='New description to set for this issue (- for stdin, '
4454 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004455 parser.add_option('-f', '--force', action='store_true',
4456 help='Delete any unpublished Gerrit edits for this issue '
4457 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004458
4459 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004460 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004461 options, args = parser.parse_args(args)
4462 _process_codereview_select_options(parser, options)
4463
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004464 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004465 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004466 target_issue_arg = ParseIssueNumberArgument(args[0],
4467 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004468 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004469 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004470
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004471 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004472
martiniss6eda05f2016-06-30 10:18:35 -07004473 kwargs = {
4474 'auth_config': auth_config,
4475 'codereview': options.forced_codereview,
4476 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004477 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004478 if target_issue_arg:
4479 kwargs['issue'] = target_issue_arg.issue
4480 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004481 if target_issue_arg.codereview and not options.forced_codereview:
4482 detected_codereview_from_url = True
4483 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004484
4485 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004486 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004487 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004488 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004489
4490 if detected_codereview_from_url:
4491 logging.info('canonical issue/change URL: %s (type: %s)\n',
4492 cl.GetIssueURL(), target_issue_arg.codereview)
4493
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004494 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004495
smut@google.com34fb6b12015-07-13 20:03:26 +00004496 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004497 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004498 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004499
4500 if options.new_description:
4501 text = options.new_description
4502 if text == '-':
4503 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004504 elif text == '+':
4505 base_branch = cl.GetCommonAncestorWithUpstream()
4506 change = cl.GetChange(base_branch, None, local_description=True)
4507 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004508
4509 description.set_description(text)
4510 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004511 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004512
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004513 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004514 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004515 return 0
4516
4517
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004518def CreateDescriptionFromLog(args):
4519 """Pulls out the commit log to use as a base for the CL description."""
4520 log_args = []
4521 if len(args) == 1 and not args[0].endswith('.'):
4522 log_args = [args[0] + '..']
4523 elif len(args) == 1 and args[0].endswith('...'):
4524 log_args = [args[0][:-1]]
4525 elif len(args) == 2:
4526 log_args = [args[0] + '..' + args[1]]
4527 else:
4528 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004529 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004530
4531
thestig@chromium.org44202a22014-03-11 19:22:18 +00004532def CMDlint(parser, args):
4533 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004534 parser.add_option('--filter', action='append', metavar='-x,+y',
4535 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004536 auth.add_auth_options(parser)
4537 options, args = parser.parse_args(args)
4538 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004539
4540 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004541 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004542 try:
4543 import cpplint
4544 import cpplint_chromium
4545 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004546 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004547 return 1
4548
4549 # Change the current working directory before calling lint so that it
4550 # shows the correct base.
4551 previous_cwd = os.getcwd()
4552 os.chdir(settings.GetRoot())
4553 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004554 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004555 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4556 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004557 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004558 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004559 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004560
4561 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004562 command = args + files
4563 if options.filter:
4564 command = ['--filter=' + ','.join(options.filter)] + command
4565 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004566
4567 white_regex = re.compile(settings.GetLintRegex())
4568 black_regex = re.compile(settings.GetLintIgnoreRegex())
4569 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4570 for filename in filenames:
4571 if white_regex.match(filename):
4572 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004573 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004574 else:
4575 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4576 extra_check_functions)
4577 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004578 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004579 finally:
4580 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004581 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004582 if cpplint._cpplint_state.error_count != 0:
4583 return 1
4584 return 0
4585
4586
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004587def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004588 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004589 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004590 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004591 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004592 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004593 auth.add_auth_options(parser)
4594 options, args = parser.parse_args(args)
4595 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004596
sbc@chromium.org71437c02015-04-09 19:29:40 +00004597 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004598 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004599 return 1
4600
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004601 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004602 if args:
4603 base_branch = args[0]
4604 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004605 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004606 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004607
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004608 cl.RunHook(
4609 committing=not options.upload,
4610 may_prompt=False,
4611 verbose=options.verbose,
4612 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004613 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004614
4615
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004616def GenerateGerritChangeId(message):
4617 """Returns Ixxxxxx...xxx change id.
4618
4619 Works the same way as
4620 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4621 but can be called on demand on all platforms.
4622
4623 The basic idea is to generate git hash of a state of the tree, original commit
4624 message, author/committer info and timestamps.
4625 """
4626 lines = []
4627 tree_hash = RunGitSilent(['write-tree'])
4628 lines.append('tree %s' % tree_hash.strip())
4629 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4630 if code == 0:
4631 lines.append('parent %s' % parent.strip())
4632 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4633 lines.append('author %s' % author.strip())
4634 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4635 lines.append('committer %s' % committer.strip())
4636 lines.append('')
4637 # Note: Gerrit's commit-hook actually cleans message of some lines and
4638 # whitespace. This code is not doing this, but it clearly won't decrease
4639 # entropy.
4640 lines.append(message)
4641 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4642 stdin='\n'.join(lines))
4643 return 'I%s' % change_hash.strip()
4644
4645
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004646def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004647 """Computes the remote branch ref to use for the CL.
4648
4649 Args:
4650 remote (str): The git remote for the CL.
4651 remote_branch (str): The git remote branch for the CL.
4652 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004653 """
4654 if not (remote and remote_branch):
4655 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004656
wittman@chromium.org455dc922015-01-26 20:15:50 +00004657 if target_branch:
4658 # Cannonicalize branch references to the equivalent local full symbolic
4659 # refs, which are then translated into the remote full symbolic refs
4660 # below.
4661 if '/' not in target_branch:
4662 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4663 else:
4664 prefix_replacements = (
4665 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4666 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4667 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4668 )
4669 match = None
4670 for regex, replacement in prefix_replacements:
4671 match = re.search(regex, target_branch)
4672 if match:
4673 remote_branch = target_branch.replace(match.group(0), replacement)
4674 break
4675 if not match:
4676 # This is a branch path but not one we recognize; use as-is.
4677 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004678 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4679 # Handle the refs that need to land in different refs.
4680 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004681
wittman@chromium.org455dc922015-01-26 20:15:50 +00004682 # Create the true path to the remote branch.
4683 # Does the following translation:
4684 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4685 # * refs/remotes/origin/master -> refs/heads/master
4686 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4687 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4688 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4689 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4690 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4691 'refs/heads/')
4692 elif remote_branch.startswith('refs/remotes/branch-heads'):
4693 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004694
wittman@chromium.org455dc922015-01-26 20:15:50 +00004695 return remote_branch
4696
4697
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004698def cleanup_list(l):
4699 """Fixes a list so that comma separated items are put as individual items.
4700
4701 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4702 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4703 """
4704 items = sum((i.split(',') for i in l), [])
4705 stripped_items = (i.strip() for i in items)
4706 return sorted(filter(None, stripped_items))
4707
4708
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004709@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004710def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004711 """Uploads the current changelist to codereview.
4712
4713 Can skip dependency patchset uploads for a branch by running:
4714 git config branch.branch_name.skip-deps-uploads True
4715 To unset run:
4716 git config --unset branch.branch_name.skip-deps-uploads
4717 Can also set the above globally by using the --global flag.
4718 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004719 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4720 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004721 parser.add_option('--bypass-watchlists', action='store_true',
4722 dest='bypass_watchlists',
4723 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004724 parser.add_option('-f', action='store_true', dest='force',
4725 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004726 parser.add_option('--message', '-m', dest='message',
4727 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004728 parser.add_option('-b', '--bug',
4729 help='pre-populate the bug number(s) for this issue. '
4730 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004731 parser.add_option('--message-file', dest='message_file',
4732 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004733 parser.add_option('--title', '-t', dest='title',
4734 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004735 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004736 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004737 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004738 parser.add_option('--tbrs',
4739 action='append', default=[],
4740 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004741 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004742 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004743 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004744 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004745 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004746 parser.add_option('--emulate_svn_auto_props',
4747 '--emulate-svn-auto-props',
4748 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004749 dest="emulate_svn_auto_props",
4750 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004751 parser.add_option('-c', '--use-commit-queue', action='store_true',
4752 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004753 parser.add_option('--private', action='store_true',
4754 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004755 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004756 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004757 metavar='TARGET',
4758 help='Apply CL to remote ref TARGET. ' +
4759 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004760 parser.add_option('--squash', action='store_true',
4761 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004762 parser.add_option('--no-squash', action='store_true',
4763 help='Don\'t squash multiple commits into one ' +
4764 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004765 parser.add_option('--topic', default=None,
4766 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004767 parser.add_option('--email', default=None,
4768 help='email address to use to connect to Rietveld')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004769 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4770 const='TBR', help='add a set of OWNERS to TBR')
4771 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4772 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004773 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4774 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004775 help='Send the patchset to do a CQ dry run right after '
4776 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004777 parser.add_option('--dependencies', action='store_true',
4778 help='Uploads CLs of all the local branches that depend on '
4779 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004780
rmistry@google.com2dd99862015-06-22 12:22:18 +00004781 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004782 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004783 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004784 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004785 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004786 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004787 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004788
sbc@chromium.org71437c02015-04-09 19:29:40 +00004789 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004790 return 1
4791
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004792 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004793 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004794 options.cc = cleanup_list(options.cc)
4795
tandriib80458a2016-06-23 12:20:07 -07004796 if options.message_file:
4797 if options.message:
4798 parser.error('only one of --message and --message-file allowed.')
4799 options.message = gclient_utils.FileRead(options.message_file)
4800 options.message_file = None
4801
tandrii4d0545a2016-07-06 03:56:49 -07004802 if options.cq_dry_run and options.use_commit_queue:
4803 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4804
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004805 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4806 settings.GetIsGerrit()
4807
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004808 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004809 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004810
4811
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004812@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004813def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004814 """DEPRECATED: Used to commit the current changelist via git-svn."""
4815 message = ('git-cl no longer supports committing to SVN repositories via '
4816 'git-svn. You probably want to use `git cl land` instead.')
4817 print(message)
4818 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004819
4820
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004821# Two special branches used by git cl land.
4822MERGE_BRANCH = 'git-cl-commit'
4823CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4824
4825
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004826@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004827def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004828 """Commits the current changelist via git.
4829
4830 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4831 upstream and closes the issue automatically and atomically.
4832
4833 Otherwise (in case of Rietveld):
4834 Squashes branch into a single commit.
4835 Updates commit message with metadata (e.g. pointer to review).
4836 Pushes the code upstream.
4837 Updates review and closes.
4838 """
4839 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4840 help='bypass upload presubmit hook')
4841 parser.add_option('-m', dest='message',
4842 help="override review description")
4843 parser.add_option('-f', action='store_true', dest='force',
4844 help="force yes to questions (don't prompt)")
4845 parser.add_option('-c', dest='contributor',
4846 help="external contributor for patch (appended to " +
4847 "description and used as author for git). Should be " +
4848 "formatted as 'First Last <email@example.com>'")
4849 add_git_similarity(parser)
4850 auth.add_auth_options(parser)
4851 (options, args) = parser.parse_args(args)
4852 auth_config = auth.extract_auth_config_from_options(options)
4853
4854 cl = Changelist(auth_config=auth_config)
4855
4856 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4857 if cl.IsGerrit():
4858 if options.message:
4859 # This could be implemented, but it requires sending a new patch to
4860 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4861 # Besides, Gerrit has the ability to change the commit message on submit
4862 # automatically, thus there is no need to support this option (so far?).
4863 parser.error('-m MESSAGE option is not supported for Gerrit.')
4864 if options.contributor:
4865 parser.error(
4866 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4867 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4868 'the contributor\'s "name <email>". If you can\'t upload such a '
4869 'commit for review, contact your repository admin and request'
4870 '"Forge-Author" permission.')
4871 if not cl.GetIssue():
4872 DieWithError('You must upload the change first to Gerrit.\n'
4873 ' If you would rather have `git cl land` upload '
4874 'automatically for you, see http://crbug.com/642759')
4875 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4876 options.verbose)
4877
4878 current = cl.GetBranch()
4879 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4880 if remote == '.':
4881 print()
4882 print('Attempting to push branch %r into another local branch!' % current)
4883 print()
4884 print('Either reparent this branch on top of origin/master:')
4885 print(' git reparent-branch --root')
4886 print()
4887 print('OR run `git rebase-update` if you think the parent branch is ')
4888 print('already committed.')
4889 print()
4890 print(' Current parent: %r' % upstream_branch)
4891 return 1
4892
4893 if not args:
4894 # Default to merging against our best guess of the upstream branch.
4895 args = [cl.GetUpstreamBranch()]
4896
4897 if options.contributor:
4898 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4899 print("Please provide contibutor as 'First Last <email@example.com>'")
4900 return 1
4901
4902 base_branch = args[0]
4903
4904 if git_common.is_dirty_git_tree('land'):
4905 return 1
4906
4907 # This rev-list syntax means "show all commits not in my branch that
4908 # are in base_branch".
4909 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4910 base_branch]).splitlines()
4911 if upstream_commits:
4912 print('Base branch "%s" has %d commits '
4913 'not in this branch.' % (base_branch, len(upstream_commits)))
4914 print('Run "git merge %s" before attempting to land.' % base_branch)
4915 return 1
4916
4917 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4918 if not options.bypass_hooks:
4919 author = None
4920 if options.contributor:
4921 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4922 hook_results = cl.RunHook(
4923 committing=True,
4924 may_prompt=not options.force,
4925 verbose=options.verbose,
4926 change=cl.GetChange(merge_base, author))
4927 if not hook_results.should_continue():
4928 return 1
4929
4930 # Check the tree status if the tree status URL is set.
4931 status = GetTreeStatus()
4932 if 'closed' == status:
4933 print('The tree is closed. Please wait for it to reopen. Use '
4934 '"git cl land --bypass-hooks" to commit on a closed tree.')
4935 return 1
4936 elif 'unknown' == status:
4937 print('Unable to determine tree status. Please verify manually and '
4938 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4939 return 1
4940
4941 change_desc = ChangeDescription(options.message)
4942 if not change_desc.description and cl.GetIssue():
4943 change_desc = ChangeDescription(cl.GetDescription())
4944
4945 if not change_desc.description:
4946 if not cl.GetIssue() and options.bypass_hooks:
4947 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4948 else:
4949 print('No description set.')
4950 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4951 return 1
4952
4953 # Keep a separate copy for the commit message, because the commit message
4954 # contains the link to the Rietveld issue, while the Rietveld message contains
4955 # the commit viewvc url.
4956 if cl.GetIssue():
Aaron Gablea1bab272017-04-11 16:38:18 -07004957 change_desc.update_reviewers(
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004958 get_approving_reviewers(cl.GetIssueProperties()), [])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004959
4960 commit_desc = ChangeDescription(change_desc.description)
4961 if cl.GetIssue():
4962 # Xcode won't linkify this URL unless there is a non-whitespace character
4963 # after it. Add a period on a new line to circumvent this. Also add a space
4964 # before the period to make sure that Gitiles continues to correctly resolve
4965 # the URL.
4966 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4967 if options.contributor:
4968 commit_desc.append_footer('Patch from %s.' % options.contributor)
4969
4970 print('Description:')
4971 print(commit_desc.description)
4972
4973 branches = [merge_base, cl.GetBranchRef()]
4974 if not options.force:
4975 print_stats(options.similarity, options.find_copies, branches)
4976
4977 # We want to squash all this branch's commits into one commit with the proper
4978 # description. We do this by doing a "reset --soft" to the base branch (which
4979 # keeps the working copy the same), then landing that.
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004980 # Delete the special branches if they exist.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004981 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4982 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4983 result = RunGitWithCode(showref_cmd)
4984 if result[0] == 0:
4985 RunGit(['branch', '-D', branch])
4986
4987 # We might be in a directory that's present in this branch but not in the
4988 # trunk. Move up to the top of the tree so that git commands that expect a
4989 # valid CWD won't fail after we check out the merge branch.
4990 rel_base_path = settings.GetRelativeRoot()
4991 if rel_base_path:
4992 os.chdir(rel_base_path)
4993
4994 # Stuff our change into the merge branch.
4995 # We wrap in a try...finally block so if anything goes wrong,
4996 # we clean up the branches.
4997 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004998 revision = None
4999 try:
5000 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
5001 RunGit(['reset', '--soft', merge_base])
5002 if options.contributor:
5003 RunGit(
5004 [
5005 'commit', '--author', options.contributor,
5006 '-m', commit_desc.description,
5007 ])
5008 else:
5009 RunGit(['commit', '-m', commit_desc.description])
5010
5011 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
5012 mirror = settings.GetGitMirror(remote)
5013 if mirror:
5014 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005015 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005016 else:
5017 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005018 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005019 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
5020
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005021 retcode = PushToGitWithAutoRebase(
5022 pushurl, branch, commit_desc.description, git_numberer_enabled)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005023 if retcode == 0:
5024 revision = RunGit(['rev-parse', 'HEAD']).strip()
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005025 if git_numberer_enabled:
5026 change_desc = ChangeDescription(
5027 RunGit(['show', '-s', '--format=%B', 'HEAD']).strip())
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005028 except: # pylint: disable=bare-except
5029 if _IS_BEING_TESTED:
5030 logging.exception('this is likely your ACTUAL cause of test failure.\n'
5031 + '-' * 30 + '8<' + '-' * 30)
5032 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
5033 raise
5034 finally:
5035 # And then swap back to the original branch and clean up.
5036 RunGit(['checkout', '-q', cl.GetBranch()])
5037 RunGit(['branch', '-D', MERGE_BRANCH])
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005038 RunGit(['branch', '-D', CHERRY_PICK_BRANCH], error_ok=True)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005039
5040 if not revision:
5041 print('Failed to push. If this persists, please file a bug.')
5042 return 1
5043
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005044 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005045 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005046 if viewvc_url and revision:
5047 change_desc.append_footer(
5048 'Committed: %s%s' % (viewvc_url, revision))
5049 elif revision:
5050 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005051 print('Closing issue '
5052 '(you may be prompted for your codereview password)...')
5053 cl.UpdateDescription(change_desc.description)
5054 cl.CloseIssue()
5055 props = cl.GetIssueProperties()
5056 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005057 comment = "Committed patchset #%d (id:%d) manually as %s" % (
5058 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005059 if options.bypass_hooks:
5060 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
5061 else:
5062 comment += ' (presubmit successful).'
5063 cl.RpcServer().add_comment(cl.GetIssue(), comment)
5064
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005065 if os.path.isfile(POSTUPSTREAM_HOOK):
5066 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
5067
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005068 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005069
5070
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005071def PushToGitWithAutoRebase(remote, branch, original_description,
5072 git_numberer_enabled, max_attempts=3):
5073 """Pushes current HEAD commit on top of remote's branch.
5074
5075 Attempts to fetch and autorebase on push failures.
5076 Adds git number footers on the fly.
5077
5078 Returns integer code from last command.
5079 """
5080 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5081 code = 0
5082 attempts_left = max_attempts
5083 while attempts_left:
5084 attempts_left -= 1
5085 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5086
5087 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5088 # If fetch fails, retry.
5089 print('Fetching %s/%s...' % (remote, branch))
5090 code, out = RunGitWithCode(
5091 ['retry', 'fetch', remote,
5092 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5093 if code:
5094 print('Fetch failed with exit code %d.' % code)
5095 print(out.strip())
5096 continue
5097
5098 print('Cherry-picking commit on top of latest %s' % branch)
5099 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5100 suppress_stderr=True)
5101 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5102 code, out = RunGitWithCode(['cherry-pick', cherry])
5103 if code:
5104 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5105 'the following files have merge conflicts:' %
5106 (branch, parent_hash))
5107 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
5108 print('Please rebase your patch and try again.')
5109 RunGitWithCode(['cherry-pick', '--abort'])
5110 break
5111
5112 commit_desc = ChangeDescription(original_description)
5113 if git_numberer_enabled:
5114 logging.debug('Adding git number footers')
5115 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5116 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5117 branch)
5118 # Ensure timestamps are monotonically increasing.
5119 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5120 _get_committer_timestamp('HEAD'))
5121 _git_amend_head(commit_desc.description, timestamp)
5122
5123 code, out = RunGitWithCode(
5124 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5125 print(out)
5126 if code == 0:
5127 break
5128 if IsFatalPushFailure(out):
5129 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005130 'user.email are correct and you have push access to the repo.\n'
5131 'Hint: run command below to diangose common Git/Gerrit credential '
5132 'problems:\n'
5133 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005134 break
5135 return code
5136
5137
5138def IsFatalPushFailure(push_stdout):
5139 """True if retrying push won't help."""
5140 return '(prohibited by Gerrit)' in push_stdout
5141
5142
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005143@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005144def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005145 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005146 parser.add_option('-b', dest='newbranch',
5147 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005148 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005149 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005150 parser.add_option('-d', '--directory', action='store', metavar='DIR',
5151 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005152 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005153 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005154 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005155 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005156 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005157 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005158
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005159
5160 group = optparse.OptionGroup(
5161 parser,
5162 'Options for continuing work on the current issue uploaded from a '
5163 'different clone (e.g. different machine). Must be used independently '
5164 'from the other options. No issue number should be specified, and the '
5165 'branch must have an issue number associated with it')
5166 group.add_option('--reapply', action='store_true', dest='reapply',
5167 help='Reset the branch and reapply the issue.\n'
5168 'CAUTION: This will undo any local changes in this '
5169 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005170
5171 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005172 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005173 parser.add_option_group(group)
5174
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005175 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005176 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005177 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005178 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005179 auth_config = auth.extract_auth_config_from_options(options)
5180
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005181 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005182 if options.newbranch:
5183 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005184 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005185 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005186
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005187 cl = Changelist(auth_config=auth_config,
5188 codereview=options.forced_codereview)
5189 if not cl.GetIssue():
5190 parser.error('current branch must have an associated issue')
5191
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005192 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005193 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005194 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005195
5196 RunGit(['reset', '--hard', upstream])
5197 if options.pull:
5198 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005199
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005200 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5201 options.directory)
5202
5203 if len(args) != 1 or not args[0]:
5204 parser.error('Must specify issue number or url')
5205
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005206 target_issue_arg = ParseIssueNumberArgument(args[0],
5207 options.forced_codereview)
5208 if not target_issue_arg.valid:
5209 parser.error('invalid codereview url or CL id')
5210
5211 cl_kwargs = {
5212 'auth_config': auth_config,
5213 'codereview_host': target_issue_arg.hostname,
5214 'codereview': options.forced_codereview,
5215 }
5216 detected_codereview_from_url = False
5217 if target_issue_arg.codereview and not options.forced_codereview:
5218 detected_codereview_from_url = True
5219 cl_kwargs['codereview'] = target_issue_arg.codereview
5220 cl_kwargs['issue'] = target_issue_arg.issue
5221
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005222 # We don't want uncommitted changes mixed up with the patch.
5223 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005224 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005225
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005226 if options.newbranch:
5227 if options.force:
5228 RunGit(['branch', '-D', options.newbranch],
5229 stderr=subprocess2.PIPE, error_ok=True)
5230 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07005231 elif not GetCurrentBranch():
5232 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005233
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005234 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005235
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005236 if cl.IsGerrit():
5237 if options.reject:
5238 parser.error('--reject is not supported with Gerrit codereview.')
5239 if options.nocommit:
5240 parser.error('--nocommit is not supported with Gerrit codereview.')
5241 if options.directory:
5242 parser.error('--directory is not supported with Gerrit codereview.')
5243
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005244 if detected_codereview_from_url:
5245 print('canonical issue/change URL: %s (type: %s)\n' %
5246 (cl.GetIssueURL(), target_issue_arg.codereview))
5247
5248 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
5249 options.nocommit, options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005250
5251
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005252def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005253 """Fetches the tree status and returns either 'open', 'closed',
5254 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005255 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005256 if url:
5257 status = urllib2.urlopen(url).read().lower()
5258 if status.find('closed') != -1 or status == '0':
5259 return 'closed'
5260 elif status.find('open') != -1 or status == '1':
5261 return 'open'
5262 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005263 return 'unset'
5264
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005265
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005266def GetTreeStatusReason():
5267 """Fetches the tree status from a json url and returns the message
5268 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005269 url = settings.GetTreeStatusUrl()
5270 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005271 connection = urllib2.urlopen(json_url)
5272 status = json.loads(connection.read())
5273 connection.close()
5274 return status['message']
5275
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005276
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005277def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005278 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005279 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005280 status = GetTreeStatus()
5281 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005282 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005283 return 2
5284
vapiera7fbd5a2016-06-16 09:17:49 -07005285 print('The tree is %s' % status)
5286 print()
5287 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005288 if status != 'open':
5289 return 1
5290 return 0
5291
5292
maruel@chromium.org15192402012-09-06 12:38:29 +00005293def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005294 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005295 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005296 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005297 '-b', '--bot', action='append',
5298 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5299 'times to specify multiple builders. ex: '
5300 '"-b win_rel -b win_layout". See '
5301 'the try server waterfall for the builders name and the tests '
5302 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005303 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005304 '-B', '--bucket', default='',
5305 help=('Buildbucket bucket to send the try requests.'))
5306 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005307 '-m', '--master', default='',
5308 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005309 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005310 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005311 help='Revision to use for the try job; default: the revision will '
5312 'be determined by the try recipe that builder runs, which usually '
5313 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005314 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005315 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005316 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005317 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005318 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005319 '--project',
5320 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005321 'in recipe to determine to which repository or directory to '
5322 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005323 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005324 '-p', '--property', dest='properties', action='append', default=[],
5325 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005326 'key2=value2 etc. The value will be treated as '
5327 'json if decodable, or as string otherwise. '
5328 'NOTE: using this may make your try job not usable for CQ, '
5329 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005330 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005331 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5332 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005333 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005334 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005335 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005336 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005337
machenbach@chromium.org45453142015-09-15 08:45:22 +00005338 # Make sure that all properties are prop=value pairs.
5339 bad_params = [x for x in options.properties if '=' not in x]
5340 if bad_params:
5341 parser.error('Got properties with missing "=": %s' % bad_params)
5342
maruel@chromium.org15192402012-09-06 12:38:29 +00005343 if args:
5344 parser.error('Unknown arguments: %s' % args)
5345
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005346 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00005347 if not cl.GetIssue():
5348 parser.error('Need to upload first')
5349
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005350 if cl.IsGerrit():
5351 # HACK: warm up Gerrit change detail cache to save on RPCs.
5352 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5353
tandriie113dfd2016-10-11 10:20:12 -07005354 error_message = cl.CannotTriggerTryJobReason()
5355 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005356 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005357
borenet6c0efe62016-10-19 08:13:29 -07005358 if options.bucket and options.master:
5359 parser.error('Only one of --bucket and --master may be used.')
5360
qyearsley1fdfcb62016-10-24 13:22:03 -07005361 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005362
qyearsleydd49f942016-10-28 11:57:22 -07005363 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5364 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005365 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005366 if options.verbose:
5367 print('git cl try with no bots now defaults to CQ Dry Run.')
5368 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00005369
borenet6c0efe62016-10-19 08:13:29 -07005370 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005371 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005372 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005373 'of bot requires an initial job from a parent (usually a builder). '
5374 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005375 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005376 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005377
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005378 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005379 # TODO(tandrii): Checking local patchset against remote patchset is only
5380 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5381 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005382 print('Warning: Codereview server has newer patchsets (%s) than most '
5383 'recent upload from local checkout (%s). Did a previous upload '
5384 'fail?\n'
5385 'By default, git cl try uses the latest patchset from '
5386 'codereview, continuing to use patchset %s.\n' %
5387 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005388
tandrii568043b2016-10-11 07:49:18 -07005389 try:
borenet6c0efe62016-10-19 08:13:29 -07005390 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5391 patchset)
tandrii568043b2016-10-11 07:49:18 -07005392 except BuildbucketResponseException as ex:
5393 print('ERROR: %s' % ex)
5394 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005395 return 0
5396
5397
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005398def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005399 """Prints info about try jobs associated with current CL."""
5400 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005401 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005402 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005403 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005404 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005405 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005406 '--color', action='store_true', default=setup_color.IS_TTY,
5407 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005408 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005409 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5410 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005411 group.add_option(
5412 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005413 parser.add_option_group(group)
5414 auth.add_auth_options(parser)
5415 options, args = parser.parse_args(args)
5416 if args:
5417 parser.error('Unrecognized args: %s' % ' '.join(args))
5418
5419 auth_config = auth.extract_auth_config_from_options(options)
5420 cl = Changelist(auth_config=auth_config)
5421 if not cl.GetIssue():
5422 parser.error('Need to upload first')
5423
tandrii221ab252016-10-06 08:12:04 -07005424 patchset = options.patchset
5425 if not patchset:
5426 patchset = cl.GetMostRecentPatchset()
5427 if not patchset:
5428 parser.error('Codereview doesn\'t know about issue %s. '
5429 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005430 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005431 cl.GetIssue())
5432
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005433 # TODO(tandrii): Checking local patchset against remote patchset is only
5434 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5435 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005436 print('Warning: Codereview server has newer patchsets (%s) than most '
5437 'recent upload from local checkout (%s). Did a previous upload '
5438 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005439 'By default, git cl try-results uses the latest patchset from '
5440 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005441 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005442 try:
tandrii221ab252016-10-06 08:12:04 -07005443 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005444 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005445 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005446 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005447 if options.json:
5448 write_try_results_json(options.json, jobs)
5449 else:
5450 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005451 return 0
5452
5453
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005454@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005455def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005456 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005457 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005458 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005459 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005460
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005461 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005462 if args:
5463 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005464 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005465 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005466 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005467 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005468
5469 # Clear configured merge-base, if there is one.
5470 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005471 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005472 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005473 return 0
5474
5475
thestig@chromium.org00858c82013-12-02 23:08:03 +00005476def CMDweb(parser, args):
5477 """Opens the current CL in the web browser."""
5478 _, args = parser.parse_args(args)
5479 if args:
5480 parser.error('Unrecognized args: %s' % ' '.join(args))
5481
5482 issue_url = Changelist().GetIssueURL()
5483 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005484 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005485 return 1
5486
5487 webbrowser.open(issue_url)
5488 return 0
5489
5490
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005491def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005492 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005493 parser.add_option('-d', '--dry-run', action='store_true',
5494 help='trigger in dry run mode')
5495 parser.add_option('-c', '--clear', action='store_true',
5496 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005497 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005498 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005499 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005500 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005501 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005502 if args:
5503 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005504 if options.dry_run and options.clear:
5505 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5506
iannuccie53c9352016-08-17 14:40:40 -07005507 cl = Changelist(auth_config=auth_config, issue=options.issue,
5508 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005509 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005510 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005511 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005512 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005513 state = _CQState.DRY_RUN
5514 else:
5515 state = _CQState.COMMIT
5516 if not cl.GetIssue():
5517 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005518 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005519 return 0
5520
5521
groby@chromium.org411034a2013-02-26 15:12:01 +00005522def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005523 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005524 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005525 auth.add_auth_options(parser)
5526 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005527 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005528 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005529 if args:
5530 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005531 cl = Changelist(auth_config=auth_config, issue=options.issue,
5532 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005533 # Ensure there actually is an issue to close.
5534 cl.GetDescription()
5535 cl.CloseIssue()
5536 return 0
5537
5538
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005539def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005540 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005541 parser.add_option(
5542 '--stat',
5543 action='store_true',
5544 dest='stat',
5545 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005546 auth.add_auth_options(parser)
5547 options, args = parser.parse_args(args)
5548 auth_config = auth.extract_auth_config_from_options(options)
5549 if args:
5550 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005551
5552 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005553 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005554 # Staged changes would be committed along with the patch from last
5555 # upload, hence counted toward the "last upload" side in the final
5556 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005557 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005558 return 1
5559
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005560 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005561 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005562 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005563 if not issue:
5564 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005565 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005566 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005567
5568 # Create a new branch based on the merge-base
5569 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005570 # Clear cached branch in cl object, to avoid overwriting original CL branch
5571 # properties.
5572 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005573 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005574 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005575 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005576 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005577 return rtn
5578
wychen@chromium.org06928532015-02-03 02:11:29 +00005579 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005580 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005581 cmd = ['git', 'diff']
5582 if options.stat:
5583 cmd.append('--stat')
5584 cmd.extend([TMP_BRANCH, branch, '--'])
5585 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005586 finally:
5587 RunGit(['checkout', '-q', branch])
5588 RunGit(['branch', '-D', TMP_BRANCH])
5589
5590 return 0
5591
5592
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005593def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005594 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005595 parser.add_option(
5596 '--no-color',
5597 action='store_true',
5598 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005599 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005600 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005601 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005602
5603 author = RunGit(['config', 'user.email']).strip() or None
5604
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005605 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005606
5607 if args:
5608 if len(args) > 1:
5609 parser.error('Unknown args')
5610 base_branch = args[0]
5611 else:
5612 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005613 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005614
5615 change = cl.GetChange(base_branch, None)
5616 return owners_finder.OwnersFinder(
5617 [f.LocalPath() for f in
5618 cl.GetChange(base_branch, None).AffectedFiles()],
Jochen Eisinger72606f82017-04-04 10:44:18 +02005619 change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02005620 author, fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005621 disable_color=options.no_color,
5622 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005623
5624
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005625def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005626 """Generates a diff command."""
5627 # Generate diff for the current branch's changes.
5628 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005629 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005630
5631 if args:
5632 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005633 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005634 diff_cmd.append(arg)
5635 else:
5636 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005637
5638 return diff_cmd
5639
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005640
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005641def MatchingFileType(file_name, extensions):
5642 """Returns true if the file name ends with one of the given extensions."""
5643 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005644
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005645
enne@chromium.org555cfe42014-01-29 18:21:39 +00005646@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005647def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005648 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005649 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005650 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005651 parser.add_option('--full', action='store_true',
5652 help='Reformat the full content of all touched files')
5653 parser.add_option('--dry-run', action='store_true',
5654 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005655 parser.add_option('--python', action='store_true',
5656 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005657 parser.add_option('--js', action='store_true',
5658 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005659 parser.add_option('--diff', action='store_true',
5660 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005661 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005662
Daniel Chengc55eecf2016-12-30 03:11:02 -08005663 # Normalize any remaining args against the current path, so paths relative to
5664 # the current directory are still resolved as expected.
5665 args = [os.path.join(os.getcwd(), arg) for arg in args]
5666
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005667 # git diff generates paths against the root of the repository. Change
5668 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005669 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005670 if rel_base_path:
5671 os.chdir(rel_base_path)
5672
digit@chromium.org29e47272013-05-17 17:01:46 +00005673 # Grab the merge-base commit, i.e. the upstream commit of the current
5674 # branch when it was created or the last time it was rebased. This is
5675 # to cover the case where the user may have called "git fetch origin",
5676 # moving the origin branch to a newer commit, but hasn't rebased yet.
5677 upstream_commit = None
5678 cl = Changelist()
5679 upstream_branch = cl.GetUpstreamBranch()
5680 if upstream_branch:
5681 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5682 upstream_commit = upstream_commit.strip()
5683
5684 if not upstream_commit:
5685 DieWithError('Could not find base commit for this branch. '
5686 'Are you in detached state?')
5687
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005688 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5689 diff_output = RunGit(changed_files_cmd)
5690 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005691 # Filter out files deleted by this CL
5692 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005693
Christopher Lamc5ba6922017-01-24 11:19:14 +11005694 if opts.js:
5695 CLANG_EXTS.append('.js')
5696
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005697 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5698 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5699 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005700 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005701
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005702 top_dir = os.path.normpath(
5703 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5704
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005705 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5706 # formatted. This is used to block during the presubmit.
5707 return_value = 0
5708
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005709 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005710 # Locate the clang-format binary in the checkout
5711 try:
5712 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005713 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005714 DieWithError(e)
5715
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005716 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005717 cmd = [clang_format_tool]
5718 if not opts.dry_run and not opts.diff:
5719 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005720 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005721 if opts.diff:
5722 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005723 else:
5724 env = os.environ.copy()
5725 env['PATH'] = str(os.path.dirname(clang_format_tool))
5726 try:
5727 script = clang_format.FindClangFormatScriptInChromiumTree(
5728 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005729 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005730 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005731
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005732 cmd = [sys.executable, script, '-p0']
5733 if not opts.dry_run and not opts.diff:
5734 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005735
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005736 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5737 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005738
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005739 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5740 if opts.diff:
5741 sys.stdout.write(stdout)
5742 if opts.dry_run and len(stdout) > 0:
5743 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005744
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005745 # Similar code to above, but using yapf on .py files rather than clang-format
5746 # on C/C++ files
5747 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005748 yapf_tool = gclient_utils.FindExecutable('yapf')
5749 if yapf_tool is None:
5750 DieWithError('yapf not found in PATH')
5751
5752 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005753 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005754 cmd = [yapf_tool]
5755 if not opts.dry_run and not opts.diff:
5756 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005757 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005758 if opts.diff:
5759 sys.stdout.write(stdout)
5760 else:
5761 # TODO(sbc): yapf --lines mode still has some issues.
5762 # https://github.com/google/yapf/issues/154
5763 DieWithError('--python currently only works with --full')
5764
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005765 # Dart's formatter does not have the nice property of only operating on
5766 # modified chunks, so hard code full.
5767 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005768 try:
5769 command = [dart_format.FindDartFmtToolInChromiumTree()]
5770 if not opts.dry_run and not opts.diff:
5771 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005772 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005773
ppi@chromium.org6593d932016-03-03 15:41:15 +00005774 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005775 if opts.dry_run and stdout:
5776 return_value = 2
5777 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005778 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5779 'found in this checkout. Files in other languages are still '
5780 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005781
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005782 # Format GN build files. Always run on full build files for canonical form.
5783 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005784 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005785 if opts.dry_run or opts.diff:
5786 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005787 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005788 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5789 shell=sys.platform == 'win32',
5790 cwd=top_dir)
5791 if opts.dry_run and gn_ret == 2:
5792 return_value = 2 # Not formatted.
5793 elif opts.diff and gn_ret == 2:
5794 # TODO this should compute and print the actual diff.
5795 print("This change has GN build file diff for " + gn_diff_file)
5796 elif gn_ret != 0:
5797 # For non-dry run cases (and non-2 return values for dry-run), a
5798 # nonzero error code indicates a failure, probably because the file
5799 # doesn't parse.
5800 DieWithError("gn format failed on " + gn_diff_file +
5801 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005802
Steven Holte2e664bf2017-04-21 13:10:47 -07005803 for xml_dir in GetDirtyMetricsDirs(diff_files):
5804 tool_dir = os.path.join(top_dir, xml_dir)
5805 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5806 if opts.dry_run or opts.diff:
5807 cmd.append('--diff')
5808 stdout = RunCommand(cmd, cwd=top_dir)
5809 if opts.diff:
5810 sys.stdout.write(stdout)
5811 if opts.dry_run and stdout:
5812 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005813
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005814 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005815
Steven Holte2e664bf2017-04-21 13:10:47 -07005816def GetDirtyMetricsDirs(diff_files):
5817 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5818 metrics_xml_dirs = [
5819 os.path.join('tools', 'metrics', 'actions'),
5820 os.path.join('tools', 'metrics', 'histograms'),
5821 os.path.join('tools', 'metrics', 'rappor'),
5822 os.path.join('tools', 'metrics', 'ukm')]
5823 for xml_dir in metrics_xml_dirs:
5824 if any(file.startswith(xml_dir) for file in xml_diff_files):
5825 yield xml_dir
5826
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005827
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005828@subcommand.usage('<codereview url or issue id>')
5829def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005830 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005831 _, args = parser.parse_args(args)
5832
5833 if len(args) != 1:
5834 parser.print_help()
5835 return 1
5836
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005837 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005838 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005839 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005840
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005841 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005842
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005843 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005844 output = RunGit(['config', '--local', '--get-regexp',
5845 r'branch\..*\.%s' % issueprefix],
5846 error_ok=True)
5847 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005848 if issue == target_issue:
5849 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005850
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005851 branches = []
5852 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005853 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005854 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005855 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005856 return 1
5857 if len(branches) == 1:
5858 RunGit(['checkout', branches[0]])
5859 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005860 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005861 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005862 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005863 which = raw_input('Choose by index: ')
5864 try:
5865 RunGit(['checkout', branches[int(which)]])
5866 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005867 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005868 return 1
5869
5870 return 0
5871
5872
maruel@chromium.org29404b52014-09-08 22:58:00 +00005873def CMDlol(parser, args):
5874 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005875 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005876 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5877 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5878 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005879 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005880 return 0
5881
5882
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005883class OptionParser(optparse.OptionParser):
5884 """Creates the option parse and add --verbose support."""
5885 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005886 optparse.OptionParser.__init__(
5887 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005888 self.add_option(
5889 '-v', '--verbose', action='count', default=0,
5890 help='Use 2 times for more debugging info')
5891
5892 def parse_args(self, args=None, values=None):
5893 options, args = optparse.OptionParser.parse_args(self, args, values)
5894 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005895 logging.basicConfig(
5896 level=levels[min(options.verbose, len(levels) - 1)],
5897 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5898 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005899 return options, args
5900
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005901
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005902def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005903 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005904 print('\nYour python version %s is unsupported, please upgrade.\n' %
5905 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005906 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005907
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005908 # Reload settings.
5909 global settings
5910 settings = Settings()
5911
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005912 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005913 dispatcher = subcommand.CommandDispatcher(__name__)
5914 try:
5915 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005916 except auth.AuthenticationError as e:
5917 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005918 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005919 if e.code != 500:
5920 raise
5921 DieWithError(
5922 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5923 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005924 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005925
5926
5927if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005928 # These affect sys.stdout so do it outside of main() to simplify mocks in
5929 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005930 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005931 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005932 try:
5933 sys.exit(main(sys.argv[1:]))
5934 except KeyboardInterrupt:
5935 sys.stderr.write('interrupted\n')
5936 sys.exit(1)