blob: 8bb7cd09134184264b4e022cf0fe2b0f6f805f64 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000032import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000033import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000034import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000035import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000036import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037
38try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080039 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000040except ImportError:
41 pass
42
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000043from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000044from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000046import auth
skobes6468b902016-10-24 08:45:10 -070047import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000048import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000049import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000050import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000051import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000052import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000053import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000054import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000056import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000057import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000058import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000060import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000062import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000063import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064import watchlists
65
tandrii7400cf02016-06-21 08:48:07 -070066__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067
tandrii9d2c7a32016-06-22 03:42:45 -070068COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070069DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080070POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000072REFS_THAT_ALIAS_TO_OTHER_REFS = {
73 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
74 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
75}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
thestig@chromium.org44202a22014-03-11 19:22:18 +000077# Valid extensions for files we want to lint.
78DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
79DEFAULT_LINT_IGNORE_REGEX = r"$^"
80
borenet6c0efe62016-10-19 08:13:29 -070081# Buildbucket master name prefix.
82MASTER_PREFIX = 'master.'
83
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000084# Shortcut since it quickly becomes redundant.
85Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000086
maruel@chromium.orgddd59412011-11-30 14:20:38 +000087# Initialized in main()
88settings = None
89
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010090# Used by tests/git_cl_test.py to add extra logging.
91# Inside the weirdly failing test, add this:
92# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
93# And scroll up to see the strack trace printed.
94_IS_BEING_TESTED = False
95
maruel@chromium.orgddd59412011-11-30 14:20:38 +000096
Christopher Lamf732cd52017-01-24 12:40:11 +110097def DieWithError(message, change_desc=None):
98 if change_desc:
99 SaveDescriptionBackup(change_desc)
100
vapiera7fbd5a2016-06-16 09:17:49 -0700101 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 sys.exit(1)
103
104
Christopher Lamf732cd52017-01-24 12:40:11 +1100105def SaveDescriptionBackup(change_desc):
106 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
107 print('\nError after CL description prompt -- saving description to %s\n' %
108 backup_path)
109 backup_file = open(backup_path, 'w')
110 backup_file.write(change_desc.description)
111 backup_file.close()
112
113
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000114def GetNoGitPagerEnv():
115 env = os.environ.copy()
116 # 'cat' is a magical git string that disables pagers on all platforms.
117 env['GIT_PAGER'] = 'cat'
118 return env
119
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000120
bsep@chromium.org627d9002016-04-29 00:00:52 +0000121def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000122 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000123 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000124 except subprocess2.CalledProcessError as e:
125 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000126 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000127 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000128 'Command "%s" failed.\n%s' % (
129 ' '.join(args), error_message or e.stdout or ''))
130 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
133def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000134 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000135 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000136
137
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000138def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000139 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700140 if suppress_stderr:
141 stderr = subprocess2.VOID
142 else:
143 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000144 try:
tandrii5d48c322016-08-18 16:19:37 -0700145 (out, _), code = subprocess2.communicate(['git'] + args,
146 env=GetNoGitPagerEnv(),
147 stdout=subprocess2.PIPE,
148 stderr=stderr)
149 return code, out
150 except subprocess2.CalledProcessError as e:
151 logging.debug('Failed running %s', args)
152 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000153
154
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000155def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000156 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000157 return RunGitWithCode(args, suppress_stderr=True)[1]
158
159
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000160def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000161 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000163 return (version.startswith(prefix) and
164 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000165
166
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000167def BranchExists(branch):
168 """Return True if specified branch exists."""
169 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
170 suppress_stderr=True)
171 return not code
172
173
tandrii2a16b952016-10-19 07:09:44 -0700174def time_sleep(seconds):
175 # Use this so that it can be mocked in tests without interfering with python
176 # system machinery.
177 import time # Local import to discourage others from importing time globally.
178 return time.sleep(seconds)
179
180
maruel@chromium.org90541732011-04-01 17:54:18 +0000181def ask_for_data(prompt):
182 try:
183 return raw_input(prompt)
184 except KeyboardInterrupt:
185 # Hide the exception.
186 sys.exit(1)
187
188
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100189def confirm_or_exit(prefix='', action='confirm'):
190 """Asks user to press enter to continue or press Ctrl+C to abort."""
191 if not prefix or prefix.endswith('\n'):
192 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100193 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100194 mid = ' Press'
195 elif prefix.endswith(' '):
196 mid = 'press'
197 else:
198 mid = ' press'
199 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
200
201
202def ask_for_explicit_yes(prompt):
203 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
204 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
205 while True:
206 if 'yes'.startswith(result):
207 return True
208 if 'no'.startswith(result):
209 return False
210 result = ask_for_data('Please, type yes or no: ').lower()
211
212
tandrii5d48c322016-08-18 16:19:37 -0700213def _git_branch_config_key(branch, key):
214 """Helper method to return Git config key for a branch."""
215 assert branch, 'branch name is required to set git config for it'
216 return 'branch.%s.%s' % (branch, key)
217
218
219def _git_get_branch_config_value(key, default=None, value_type=str,
220 branch=False):
221 """Returns git config value of given or current branch if any.
222
223 Returns default in all other cases.
224 """
225 assert value_type in (int, str, bool)
226 if branch is False: # Distinguishing default arg value from None.
227 branch = GetCurrentBranch()
228
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000229 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700230 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000231
tandrii5d48c322016-08-18 16:19:37 -0700232 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700233 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700234 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700235 # git config also has --int, but apparently git config suffers from integer
236 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700237 args.append(_git_branch_config_key(branch, key))
238 code, out = RunGitWithCode(args)
239 if code == 0:
240 value = out.strip()
241 if value_type == int:
242 return int(value)
243 if value_type == bool:
244 return bool(value.lower() == 'true')
245 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000246 return default
247
248
tandrii5d48c322016-08-18 16:19:37 -0700249def _git_set_branch_config_value(key, value, branch=None, **kwargs):
250 """Sets the value or unsets if it's None of a git branch config.
251
252 Valid, though not necessarily existing, branch must be provided,
253 otherwise currently checked out branch is used.
254 """
255 if not branch:
256 branch = GetCurrentBranch()
257 assert branch, 'a branch name OR currently checked out branch is required'
258 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700259 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700260 if value is None:
261 args.append('--unset')
262 elif isinstance(value, bool):
263 args.append('--bool')
264 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700265 else:
tandrii33a46ff2016-08-23 05:53:40 -0700266 # git config also has --int, but apparently git config suffers from integer
267 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700268 value = str(value)
269 args.append(_git_branch_config_key(branch, key))
270 if value is not None:
271 args.append(value)
272 RunGit(args, **kwargs)
273
274
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100275def _get_committer_timestamp(commit):
276 """Returns unix timestamp as integer of a committer in a commit.
277
278 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
279 """
280 # Git also stores timezone offset, but it only affects visual display,
281 # actual point in time is defined by this timestamp only.
282 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
283
284
285def _git_amend_head(message, committer_timestamp):
286 """Amends commit with new message and desired committer_timestamp.
287
288 Sets committer timezone to UTC.
289 """
290 env = os.environ.copy()
291 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
292 return RunGit(['commit', '--amend', '-m', message], env=env)
293
294
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000295def add_git_similarity(parser):
296 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700297 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000298 help='Sets the percentage that a pair of files need to match in order to'
299 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000300 parser.add_option(
301 '--find-copies', action='store_true',
302 help='Allows git to look for copies.')
303 parser.add_option(
304 '--no-find-copies', action='store_false', dest='find_copies',
305 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000306
307 old_parser_args = parser.parse_args
308 def Parse(args):
309 options, args = old_parser_args(args)
310
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000311 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700312 options.similarity = _git_get_branch_config_value(
313 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000314 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000315 print('Note: Saving similarity of %d%% in git config.'
316 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700317 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000318
iannucci@chromium.org79540052012-10-19 23:15:26 +0000319 options.similarity = max(0, min(options.similarity, 100))
320
321 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700322 options.find_copies = _git_get_branch_config_value(
323 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000324 else:
tandrii5d48c322016-08-18 16:19:37 -0700325 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000326
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000327 return options, args
328 parser.parse_args = Parse
329
330
machenbach@chromium.org45453142015-09-15 08:45:22 +0000331def _get_properties_from_options(options):
332 properties = dict(x.split('=', 1) for x in options.properties)
333 for key, val in properties.iteritems():
334 try:
335 properties[key] = json.loads(val)
336 except ValueError:
337 pass # If a value couldn't be evaluated, treat it as a string.
338 return properties
339
340
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000341def _prefix_master(master):
342 """Convert user-specified master name to full master name.
343
344 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
345 name, while the developers always use shortened master name
346 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
347 function does the conversion for buildbucket migration.
348 """
borenet6c0efe62016-10-19 08:13:29 -0700349 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000350 return master
borenet6c0efe62016-10-19 08:13:29 -0700351 return '%s%s' % (MASTER_PREFIX, master)
352
353
354def _unprefix_master(bucket):
355 """Convert bucket name to shortened master name.
356
357 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
358 name, while the developers always use shortened master name
359 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
360 function does the conversion for buildbucket migration.
361 """
362 if bucket.startswith(MASTER_PREFIX):
363 return bucket[len(MASTER_PREFIX):]
364 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000365
366
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000367def _buildbucket_retry(operation_name, http, *args, **kwargs):
368 """Retries requests to buildbucket service and returns parsed json content."""
369 try_count = 0
370 while True:
371 response, content = http.request(*args, **kwargs)
372 try:
373 content_json = json.loads(content)
374 except ValueError:
375 content_json = None
376
377 # Buildbucket could return an error even if status==200.
378 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000379 error = content_json.get('error')
380 if error.get('code') == 403:
381 raise BuildbucketResponseException(
382 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000383 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000384 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000385 raise BuildbucketResponseException(msg)
386
387 if response.status == 200:
388 if not content_json:
389 raise BuildbucketResponseException(
390 'Buildbucket returns invalid json content: %s.\n'
391 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
392 content)
393 return content_json
394 if response.status < 500 or try_count >= 2:
395 raise httplib2.HttpLib2Error(content)
396
397 # status >= 500 means transient failures.
398 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700399 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000400 try_count += 1
401 assert False, 'unreachable'
402
403
qyearsley1fdfcb62016-10-24 13:22:03 -0700404def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700405 """Returns a dict mapping bucket names to builders and tests,
406 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700407 """
qyearsleydd49f942016-10-28 11:57:22 -0700408 # If no bots are listed, we try to get a set of builders and tests based
409 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 if not options.bot:
411 change = changelist.GetChange(
412 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700413 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700414 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700415 change=change,
416 changed_files=change.LocalPaths(),
417 repository_root=settings.GetRoot(),
418 default_presubmit=None,
419 project=None,
420 verbose=options.verbose,
421 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700422 if masters is None:
423 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100424 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700425
qyearsley1fdfcb62016-10-24 13:22:03 -0700426 if options.bucket:
427 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700428 if options.master:
429 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700430
qyearsleydd49f942016-10-28 11:57:22 -0700431 # If bots are listed but no master or bucket, then we need to find out
432 # the corresponding master for each bot.
433 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
434 if error_message:
435 option_parser.error(
436 'Tryserver master cannot be found because: %s\n'
437 'Please manually specify the tryserver master, e.g. '
438 '"-m tryserver.chromium.linux".' % error_message)
439 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700440
441
qyearsley123a4682016-10-26 09:12:17 -0700442def _get_bucket_map_for_builders(builders):
443 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700444 map_url = 'https://builders-map.appspot.com/'
445 try:
qyearsley123a4682016-10-26 09:12:17 -0700446 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700447 except urllib2.URLError as e:
448 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
449 (map_url, e))
450 except ValueError as e:
451 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700452 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700453 return None, 'Failed to build master map.'
454
qyearsley123a4682016-10-26 09:12:17 -0700455 bucket_map = {}
456 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700457 masters = builders_map.get(builder, [])
458 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700459 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700460 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700461 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700462 (builder, masters))
463 bucket = _prefix_master(masters[0])
464 bucket_map.setdefault(bucket, {})[builder] = []
465
466 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700467
468
borenet6c0efe62016-10-19 08:13:29 -0700469def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700470 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700471 """Sends a request to Buildbucket to trigger try jobs for a changelist.
472
473 Args:
474 auth_config: AuthConfig for Rietveld.
475 changelist: Changelist that the try jobs are associated with.
476 buckets: A nested dict mapping bucket names to builders to tests.
477 options: Command-line options.
478 """
tandriide281ae2016-10-12 06:02:30 -0700479 assert changelist.GetIssue(), 'CL must be uploaded first'
480 codereview_url = changelist.GetCodereviewServer()
481 assert codereview_url, 'CL must be uploaded first'
482 patchset = patchset or changelist.GetMostRecentPatchset()
483 assert patchset, 'CL must be uploaded first'
484
485 codereview_host = urlparse.urlparse(codereview_url).hostname
486 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000487 http = authenticator.authorize(httplib2.Http())
488 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700489
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000490 buildbucket_put_url = (
491 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000492 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700493 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
494 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
495 hostname=codereview_host,
496 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000497 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700498
499 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
500 shared_parameters_properties['category'] = category
501 if options.clobber:
502 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700503 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700504 if extra_properties:
505 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000506
507 batch_req_body = {'builds': []}
508 print_text = []
509 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700510 for bucket, builders_and_tests in sorted(buckets.iteritems()):
511 print_text.append('Bucket: %s' % bucket)
512 master = None
513 if bucket.startswith(MASTER_PREFIX):
514 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000515 for builder, tests in sorted(builders_and_tests.iteritems()):
516 print_text.append(' %s: %s' % (builder, tests))
517 parameters = {
518 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000519 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100520 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000521 'revision': options.revision,
522 }],
tandrii8c5a3532016-11-04 07:52:02 -0700523 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000524 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000525 if 'presubmit' in builder.lower():
526 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000527 if tests:
528 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700529
530 tags = [
531 'builder:%s' % builder,
532 'buildset:%s' % buildset,
533 'user_agent:git_cl_try',
534 ]
535 if master:
536 parameters['properties']['master'] = master
537 tags.append('master:%s' % master)
538
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000539 batch_req_body['builds'].append(
540 {
541 'bucket': bucket,
542 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000543 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700544 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000545 }
546 )
547
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000548 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700549 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 http,
551 buildbucket_put_url,
552 'PUT',
553 body=json.dumps(batch_req_body),
554 headers={'Content-Type': 'application/json'}
555 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000556 print_text.append('To see results here, run: git cl try-results')
557 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700558 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000559
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000560
tandrii221ab252016-10-06 08:12:04 -0700561def fetch_try_jobs(auth_config, changelist, buildbucket_host,
562 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700563 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564
qyearsley53f48a12016-09-01 10:45:13 -0700565 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566 """
tandrii221ab252016-10-06 08:12:04 -0700567 assert buildbucket_host
568 assert changelist.GetIssue(), 'CL must be uploaded first'
569 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
570 patchset = patchset or changelist.GetMostRecentPatchset()
571 assert patchset, 'CL must be uploaded first'
572
573 codereview_url = changelist.GetCodereviewServer()
574 codereview_host = urlparse.urlparse(codereview_url).hostname
575 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576 if authenticator.has_cached_credentials():
577 http = authenticator.authorize(httplib2.Http())
578 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700579 print('Warning: Some results might be missing because %s' %
580 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700581 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000582 http = httplib2.Http()
583
584 http.force_exception_to_status_code = True
585
tandrii221ab252016-10-06 08:12:04 -0700586 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
587 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
588 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000589 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700590 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 params = {'tag': 'buildset:%s' % buildset}
592
593 builds = {}
594 while True:
595 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700596 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000597 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700598 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000599 for build in content.get('builds', []):
600 builds[build['id']] = build
601 if 'next_cursor' in content:
602 params['start_cursor'] = content['next_cursor']
603 else:
604 break
605 return builds
606
607
qyearsleyeab3c042016-08-24 09:18:28 -0700608def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000609 """Prints nicely result of fetch_try_jobs."""
610 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700611 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000612 return
613
614 # Make a copy, because we'll be modifying builds dictionary.
615 builds = builds.copy()
616 builder_names_cache = {}
617
618 def get_builder(b):
619 try:
620 return builder_names_cache[b['id']]
621 except KeyError:
622 try:
623 parameters = json.loads(b['parameters_json'])
624 name = parameters['builder_name']
625 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700626 print('WARNING: failed to get builder name for build %s: %s' % (
627 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000628 name = None
629 builder_names_cache[b['id']] = name
630 return name
631
632 def get_bucket(b):
633 bucket = b['bucket']
634 if bucket.startswith('master.'):
635 return bucket[len('master.'):]
636 return bucket
637
638 if options.print_master:
639 name_fmt = '%%-%ds %%-%ds' % (
640 max(len(str(get_bucket(b))) for b in builds.itervalues()),
641 max(len(str(get_builder(b))) for b in builds.itervalues()))
642 def get_name(b):
643 return name_fmt % (get_bucket(b), get_builder(b))
644 else:
645 name_fmt = '%%-%ds' % (
646 max(len(str(get_builder(b))) for b in builds.itervalues()))
647 def get_name(b):
648 return name_fmt % get_builder(b)
649
650 def sort_key(b):
651 return b['status'], b.get('result'), get_name(b), b.get('url')
652
653 def pop(title, f, color=None, **kwargs):
654 """Pop matching builds from `builds` dict and print them."""
655
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000656 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000657 colorize = str
658 else:
659 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
660
661 result = []
662 for b in builds.values():
663 if all(b.get(k) == v for k, v in kwargs.iteritems()):
664 builds.pop(b['id'])
665 result.append(b)
666 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700667 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000668 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700669 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000670
671 total = len(builds)
672 pop(status='COMPLETED', result='SUCCESS',
673 title='Successes:', color=Fore.GREEN,
674 f=lambda b: (get_name(b), b.get('url')))
675 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
676 title='Infra Failures:', color=Fore.MAGENTA,
677 f=lambda b: (get_name(b), b.get('url')))
678 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
679 title='Failures:', color=Fore.RED,
680 f=lambda b: (get_name(b), b.get('url')))
681 pop(status='COMPLETED', result='CANCELED',
682 title='Canceled:', color=Fore.MAGENTA,
683 f=lambda b: (get_name(b),))
684 pop(status='COMPLETED', result='FAILURE',
685 failure_reason='INVALID_BUILD_DEFINITION',
686 title='Wrong master/builder name:', color=Fore.MAGENTA,
687 f=lambda b: (get_name(b),))
688 pop(status='COMPLETED', result='FAILURE',
689 title='Other failures:',
690 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
691 pop(status='COMPLETED',
692 title='Other finished:',
693 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
694 pop(status='STARTED',
695 title='Started:', color=Fore.YELLOW,
696 f=lambda b: (get_name(b), b.get('url')))
697 pop(status='SCHEDULED',
698 title='Scheduled:',
699 f=lambda b: (get_name(b), 'id=%s' % b['id']))
700 # The last section is just in case buildbucket API changes OR there is a bug.
701 pop(title='Other:',
702 f=lambda b: (get_name(b), 'id=%s' % b['id']))
703 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700704 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000705
706
qyearsley53f48a12016-09-01 10:45:13 -0700707def write_try_results_json(output_file, builds):
708 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
709
710 The input |builds| dict is assumed to be generated by Buildbucket.
711 Buildbucket documentation: http://goo.gl/G0s101
712 """
713
714 def convert_build_dict(build):
715 return {
716 'buildbucket_id': build.get('id'),
717 'status': build.get('status'),
718 'result': build.get('result'),
719 'bucket': build.get('bucket'),
720 'builder_name': json.loads(
721 build.get('parameters_json', '{}')).get('builder_name'),
722 'failure_reason': build.get('failure_reason'),
723 'url': build.get('url'),
724 }
725
726 converted = []
727 for _, build in sorted(builds.items()):
728 converted.append(convert_build_dict(build))
729 write_json(output_file, converted)
730
731
iannucci@chromium.org79540052012-10-19 23:15:26 +0000732def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000733 """Prints statistics about the change to the user."""
734 # --no-ext-diff is broken in some versions of Git, so try to work around
735 # this by overriding the environment (but there is still a problem if the
736 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000738 if 'GIT_EXTERNAL_DIFF' in env:
739 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000740
741 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800742 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000743 else:
744 similarity_options = ['-M%s' % similarity]
745
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000746 try:
747 stdout = sys.stdout.fileno()
748 except AttributeError:
749 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000750 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000751 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000752 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000753 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000754
755
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000756class BuildbucketResponseException(Exception):
757 pass
758
759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760class Settings(object):
761 def __init__(self):
762 self.default_server = None
763 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000764 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 self.tree_status_url = None
766 self.viewvc_url = None
767 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000768 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000769 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000770 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000771 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000772 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000773 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774
775 def LazyUpdateIfNeeded(self):
776 """Updates the settings from a codereview.settings file, if available."""
777 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000778 # The only value that actually changes the behavior is
779 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000780 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000781 error_ok=True
782 ).strip().lower()
783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000785 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786 LoadCodereviewSettingsFromFile(cr_settings_file)
787 self.updated = True
788
789 def GetDefaultServerUrl(self, error_ok=False):
790 if not self.default_server:
791 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000792 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000793 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 if error_ok:
795 return self.default_server
796 if not self.default_server:
797 error_message = ('Could not find settings file. You must configure '
798 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000799 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000800 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 return self.default_server
802
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000803 @staticmethod
804 def GetRelativeRoot():
805 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000806
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000808 if self.root is None:
809 self.root = os.path.abspath(self.GetRelativeRoot())
810 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000812 def GetGitMirror(self, remote='origin'):
813 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000814 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000815 if not os.path.isdir(local_url):
816 return None
817 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
818 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100819 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100820 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000821 if mirror.exists():
822 return mirror
823 return None
824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825 def GetTreeStatusUrl(self, error_ok=False):
826 if not self.tree_status_url:
827 error_message = ('You must configure your tree status URL by running '
828 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000829 self.tree_status_url = self._GetRietveldConfig(
830 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000831 return self.tree_status_url
832
833 def GetViewVCUrl(self):
834 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000835 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 return self.viewvc_url
837
rmistry@google.com90752582014-01-14 21:04:50 +0000838 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000839 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000840
rmistry@google.com78948ed2015-07-08 23:09:57 +0000841 def GetIsSkipDependencyUpload(self, branch_name):
842 """Returns true if specified branch should skip dep uploads."""
843 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
844 error_ok=True)
845
rmistry@google.com5626a922015-02-26 14:03:30 +0000846 def GetRunPostUploadHook(self):
847 run_post_upload_hook = self._GetRietveldConfig(
848 'run-post-upload-hook', error_ok=True)
849 return run_post_upload_hook == "True"
850
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000851 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000852 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000853
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000854 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000855 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000856
ukai@chromium.orge8077812012-02-03 03:41:46 +0000857 def GetIsGerrit(self):
858 """Return true if this repo is assosiated with gerrit code review system."""
859 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700860 self.is_gerrit = (
861 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000862 return self.is_gerrit
863
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000864 def GetSquashGerritUploads(self):
865 """Return true if uploads to Gerrit should be squashed by default."""
866 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700867 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
868 if self.squash_gerrit_uploads is None:
869 # Default is squash now (http://crbug.com/611892#c23).
870 self.squash_gerrit_uploads = not (
871 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
872 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000873 return self.squash_gerrit_uploads
874
tandriia60502f2016-06-20 02:01:53 -0700875 def GetSquashGerritUploadsOverride(self):
876 """Return True or False if codereview.settings should be overridden.
877
878 Returns None if no override has been defined.
879 """
880 # See also http://crbug.com/611892#c23
881 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
882 error_ok=True).strip()
883 if result == 'true':
884 return True
885 if result == 'false':
886 return False
887 return None
888
tandrii@chromium.org28253532016-04-14 13:46:56 +0000889 def GetGerritSkipEnsureAuthenticated(self):
890 """Return True if EnsureAuthenticated should not be done for Gerrit
891 uploads."""
892 if self.gerrit_skip_ensure_authenticated is None:
893 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000894 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000895 error_ok=True).strip() == 'true')
896 return self.gerrit_skip_ensure_authenticated
897
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000898 def GetGitEditor(self):
899 """Return the editor specified in the git config, or None if none is."""
900 if self.git_editor is None:
901 self.git_editor = self._GetConfig('core.editor', error_ok=True)
902 return self.git_editor or None
903
thestig@chromium.org44202a22014-03-11 19:22:18 +0000904 def GetLintRegex(self):
905 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
906 DEFAULT_LINT_REGEX)
907
908 def GetLintIgnoreRegex(self):
909 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
910 DEFAULT_LINT_IGNORE_REGEX)
911
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000912 def GetProject(self):
913 if not self.project:
914 self.project = self._GetRietveldConfig('project', error_ok=True)
915 return self.project
916
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000917 def _GetRietveldConfig(self, param, **kwargs):
918 return self._GetConfig('rietveld.' + param, **kwargs)
919
rmistry@google.com78948ed2015-07-08 23:09:57 +0000920 def _GetBranchConfig(self, branch_name, param, **kwargs):
921 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
922
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923 def _GetConfig(self, param, **kwargs):
924 self.LazyUpdateIfNeeded()
925 return RunGit(['config', param], **kwargs).strip()
926
927
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100928@contextlib.contextmanager
929def _get_gerrit_project_config_file(remote_url):
930 """Context manager to fetch and store Gerrit's project.config from
931 refs/meta/config branch and store it in temp file.
932
933 Provides a temporary filename or None if there was error.
934 """
935 error, _ = RunGitWithCode([
936 'fetch', remote_url,
937 '+refs/meta/config:refs/git_cl/meta/config'])
938 if error:
939 # Ref doesn't exist or isn't accessible to current user.
940 print('WARNING: failed to fetch project config for %s: %s' %
941 (remote_url, error))
942 yield None
943 return
944
945 error, project_config_data = RunGitWithCode(
946 ['show', 'refs/git_cl/meta/config:project.config'])
947 if error:
948 print('WARNING: project.config file not found')
949 yield None
950 return
951
952 with gclient_utils.temporary_directory() as tempdir:
953 project_config_file = os.path.join(tempdir, 'project.config')
954 gclient_utils.FileWrite(project_config_file, project_config_data)
955 yield project_config_file
956
957
958def _is_git_numberer_enabled(remote_url, remote_ref):
959 """Returns True if Git Numberer is enabled on this ref."""
960 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100961 KNOWN_PROJECTS_WHITELIST = [
962 'chromium/src',
963 'external/webrtc',
964 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100965 'infra/experimental',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100966 ]
967
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100968 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
969 url_parts = urlparse.urlparse(remote_url)
970 project_name = url_parts.path.lstrip('/').rstrip('git./')
971 for known in KNOWN_PROJECTS_WHITELIST:
972 if project_name.endswith(known):
973 break
974 else:
975 # Early exit to avoid extra fetches for repos that aren't using Git
976 # Numberer.
977 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100978
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100979 with _get_gerrit_project_config_file(remote_url) as project_config_file:
980 if project_config_file is None:
981 # Failed to fetch project.config, which shouldn't happen on open source
982 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100983 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100984 def get_opts(x):
985 code, out = RunGitWithCode(
986 ['config', '-f', project_config_file, '--get-all',
987 'plugin.git-numberer.validate-%s-refglob' % x])
988 if code == 0:
989 return out.strip().splitlines()
990 return []
991 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100992
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100993 logging.info('validator config enabled %s disabled %s refglobs for '
994 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000995
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100996 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100997 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100998 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100999 return True
1000 return False
1001
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001002 if match_refglobs(disabled):
1003 return False
1004 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001005
1006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007def ShortBranchName(branch):
1008 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001009 return branch.replace('refs/heads/', '', 1)
1010
1011
1012def GetCurrentBranchRef():
1013 """Returns branch ref (e.g., refs/heads/master) or None."""
1014 return RunGit(['symbolic-ref', 'HEAD'],
1015 stderr=subprocess2.VOID, error_ok=True).strip() or None
1016
1017
1018def GetCurrentBranch():
1019 """Returns current branch or None.
1020
1021 For refs/heads/* branches, returns just last part. For others, full ref.
1022 """
1023 branchref = GetCurrentBranchRef()
1024 if branchref:
1025 return ShortBranchName(branchref)
1026 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027
1028
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001029class _CQState(object):
1030 """Enum for states of CL with respect to Commit Queue."""
1031 NONE = 'none'
1032 DRY_RUN = 'dry_run'
1033 COMMIT = 'commit'
1034
1035 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1036
1037
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001038class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001039 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001040 self.issue = issue
1041 self.patchset = patchset
1042 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001043 assert codereview in (None, 'rietveld', 'gerrit')
1044 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001045
1046 @property
1047 def valid(self):
1048 return self.issue is not None
1049
1050
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001051def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001052 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1053 fail_result = _ParsedIssueNumberArgument()
1054
1055 if arg.isdigit():
1056 return _ParsedIssueNumberArgument(issue=int(arg))
1057 if not arg.startswith('http'):
1058 return fail_result
1059 url = gclient_utils.UpgradeToHttps(arg)
1060 try:
1061 parsed_url = urlparse.urlparse(url)
1062 except ValueError:
1063 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001064
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001065 if codereview is not None:
1066 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1067 return parsed or fail_result
1068
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001069 results = {}
1070 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1071 parsed = cls.ParseIssueURL(parsed_url)
1072 if parsed is not None:
1073 results[name] = parsed
1074
1075 if not results:
1076 return fail_result
1077 if len(results) == 1:
1078 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001079
1080 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1081 # This is likely Gerrit.
1082 return results['gerrit']
1083 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001084 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001085
1086
Aaron Gablea45ee112016-11-22 15:14:38 -08001087class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001088 def __init__(self, issue, url):
1089 self.issue = issue
1090 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001091 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001092
1093 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001094 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001095 self.issue, self.url)
1096
1097
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001098_CommentSummary = collections.namedtuple(
1099 '_CommentSummary', ['date', 'message', 'sender',
1100 # TODO(tandrii): these two aren't known in Gerrit.
1101 'approval', 'disapproval'])
1102
1103
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001105 """Changelist works with one changelist in local branch.
1106
1107 Supports two codereview backends: Rietveld or Gerrit, selected at object
1108 creation.
1109
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001110 Notes:
1111 * Not safe for concurrent multi-{thread,process} use.
1112 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001113 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001114 """
1115
1116 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1117 """Create a new ChangeList instance.
1118
1119 If issue is given, the codereview must be given too.
1120
1121 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1122 Otherwise, it's decided based on current configuration of the local branch,
1123 with default being 'rietveld' for backwards compatibility.
1124 See _load_codereview_impl for more details.
1125
1126 **kwargs will be passed directly to codereview implementation.
1127 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001129 global settings
1130 if not settings:
1131 # Happens when git_cl.py is used as a utility library.
1132 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001133
1134 if issue:
1135 assert codereview, 'codereview must be known, if issue is known'
1136
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 self.branchref = branchref
1138 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001139 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 self.branch = ShortBranchName(self.branchref)
1141 else:
1142 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001144 self.lookedup_issue = False
1145 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146 self.has_description = False
1147 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001148 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001150 self.cc = None
1151 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001152 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001153
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001154 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001155 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001156 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001157 assert self._codereview_impl
1158 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001159
1160 def _load_codereview_impl(self, codereview=None, **kwargs):
1161 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001162 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1163 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1164 self._codereview = codereview
1165 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001166 return
1167
1168 # Automatic selection based on issue number set for a current branch.
1169 # Rietveld takes precedence over Gerrit.
1170 assert not self.issue
1171 # Whether we find issue or not, we are doing the lookup.
1172 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001173 if self.GetBranch():
1174 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1175 issue = _git_get_branch_config_value(
1176 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1177 if issue:
1178 self._codereview = codereview
1179 self._codereview_impl = cls(self, **kwargs)
1180 self.issue = int(issue)
1181 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001182
1183 # No issue is set for this branch, so decide based on repo-wide settings.
1184 return self._load_codereview_impl(
1185 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1186 **kwargs)
1187
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001188 def IsGerrit(self):
1189 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001190
1191 def GetCCList(self):
1192 """Return the users cc'd on this CL.
1193
agable92bec4f2016-08-24 09:27:27 -07001194 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001195 """
1196 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001197 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001198 more_cc = ','.join(self.watchers)
1199 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1200 return self.cc
1201
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001202 def GetCCListWithoutDefault(self):
1203 """Return the users cc'd on this CL excluding default ones."""
1204 if self.cc is None:
1205 self.cc = ','.join(self.watchers)
1206 return self.cc
1207
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001208 def SetWatchers(self, watchers):
1209 """Set the list of email addresses that should be cc'd based on the changed
1210 files in this CL.
1211 """
1212 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213
1214 def GetBranch(self):
1215 """Returns the short branch name, e.g. 'master'."""
1216 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001217 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001218 if not branchref:
1219 return None
1220 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221 self.branch = ShortBranchName(self.branchref)
1222 return self.branch
1223
1224 def GetBranchRef(self):
1225 """Returns the full branch name, e.g. 'refs/heads/master'."""
1226 self.GetBranch() # Poke the lazy loader.
1227 return self.branchref
1228
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001229 def ClearBranch(self):
1230 """Clears cached branch data of this object."""
1231 self.branch = self.branchref = None
1232
tandrii5d48c322016-08-18 16:19:37 -07001233 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1234 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1235 kwargs['branch'] = self.GetBranch()
1236 return _git_get_branch_config_value(key, default, **kwargs)
1237
1238 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1239 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1240 assert self.GetBranch(), (
1241 'this CL must have an associated branch to %sset %s%s' %
1242 ('un' if value is None else '',
1243 key,
1244 '' if value is None else ' to %r' % value))
1245 kwargs['branch'] = self.GetBranch()
1246 return _git_set_branch_config_value(key, value, **kwargs)
1247
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001248 @staticmethod
1249 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001250 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 e.g. 'origin', 'refs/heads/master'
1252 """
1253 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001254 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1255
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001257 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001259 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1260 error_ok=True).strip()
1261 if upstream_branch:
1262 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001264 # Else, try to guess the origin remote.
1265 remote_branches = RunGit(['branch', '-r']).split()
1266 if 'origin/master' in remote_branches:
1267 # Fall back on origin/master if it exits.
1268 remote = 'origin'
1269 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001271 DieWithError(
1272 'Unable to determine default branch to diff against.\n'
1273 'Either pass complete "git diff"-style arguments, like\n'
1274 ' git cl upload origin/master\n'
1275 'or verify this branch is set up to track another \n'
1276 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277
1278 return remote, upstream_branch
1279
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001280 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001281 upstream_branch = self.GetUpstreamBranch()
1282 if not BranchExists(upstream_branch):
1283 DieWithError('The upstream for the current branch (%s) does not exist '
1284 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001285 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001286 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 def GetUpstreamBranch(self):
1289 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001292 upstream_branch = upstream_branch.replace('refs/heads/',
1293 'refs/remotes/%s/' % remote)
1294 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1295 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296 self.upstream_branch = upstream_branch
1297 return self.upstream_branch
1298
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001299 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001300 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 remote, branch = None, self.GetBranch()
1302 seen_branches = set()
1303 while branch not in seen_branches:
1304 seen_branches.add(branch)
1305 remote, branch = self.FetchUpstreamTuple(branch)
1306 branch = ShortBranchName(branch)
1307 if remote != '.' or branch.startswith('refs/remotes'):
1308 break
1309 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001310 remotes = RunGit(['remote'], error_ok=True).split()
1311 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001312 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001313 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001314 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001315 logging.warn('Could not determine which remote this change is '
1316 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001317 else:
1318 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001319 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001320 branch = 'HEAD'
1321 if branch.startswith('refs/remotes'):
1322 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001323 elif branch.startswith('refs/branch-heads/'):
1324 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001325 else:
1326 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001327 return self._remote
1328
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 def GitSanityChecks(self, upstream_git_obj):
1330 """Checks git repo status and ensures diff is from local commits."""
1331
sbc@chromium.org79706062015-01-14 21:18:12 +00001332 if upstream_git_obj is None:
1333 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001334 print('ERROR: unable to determine current branch (detached HEAD?)',
1335 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001336 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001337 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001338 return False
1339
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001340 # Verify the commit we're diffing against is in our current branch.
1341 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1342 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1343 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001344 print('ERROR: %s is not in the current branch. You may need to rebase '
1345 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001346 return False
1347
1348 # List the commits inside the diff, and verify they are all local.
1349 commits_in_diff = RunGit(
1350 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1351 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1352 remote_branch = remote_branch.strip()
1353 if code != 0:
1354 _, remote_branch = self.GetRemoteBranch()
1355
1356 commits_in_remote = RunGit(
1357 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1358
1359 common_commits = set(commits_in_diff) & set(commits_in_remote)
1360 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001361 print('ERROR: Your diff contains %d commits already in %s.\n'
1362 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1363 'the diff. If you are using a custom git flow, you can override'
1364 ' the reference used for this check with "git config '
1365 'gitcl.remotebranch <git-ref>".' % (
1366 len(common_commits), remote_branch, upstream_git_obj),
1367 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001368 return False
1369 return True
1370
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001371 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001372 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001373
1374 Returns None if it is not set.
1375 """
tandrii5d48c322016-08-18 16:19:37 -07001376 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001377
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378 def GetRemoteUrl(self):
1379 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1380
1381 Returns None if there is no remote.
1382 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001383 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001384 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1385
1386 # If URL is pointing to a local directory, it is probably a git cache.
1387 if os.path.isdir(url):
1388 url = RunGit(['config', 'remote.%s.url' % remote],
1389 error_ok=True,
1390 cwd=url).strip()
1391 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001393 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001394 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001395 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001396 self.issue = self._GitGetBranchConfigValue(
1397 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001398 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 return self.issue
1400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 def GetIssueURL(self):
1402 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001403 issue = self.GetIssue()
1404 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001405 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001406 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001408 def GetDescription(self, pretty=False, force=False):
1409 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001411 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 self.has_description = True
1413 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001414 # Set width to 72 columns + 2 space indent.
1415 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001417 lines = self.description.splitlines()
1418 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 return self.description
1420
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001421 def GetDescriptionFooters(self):
1422 """Returns (non_footer_lines, footers) for the commit message.
1423
1424 Returns:
1425 non_footer_lines (list(str)) - Simple list of description lines without
1426 any footer. The lines do not contain newlines, nor does the list contain
1427 the empty line between the message and the footers.
1428 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1429 [("Change-Id", "Ideadbeef...."), ...]
1430 """
1431 raw_description = self.GetDescription()
1432 msg_lines, _, footers = git_footers.split_footers(raw_description)
1433 if footers:
1434 msg_lines = msg_lines[:len(msg_lines)-1]
1435 return msg_lines, footers
1436
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001438 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001439 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001440 self.patchset = self._GitGetBranchConfigValue(
1441 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001442 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 return self.patchset
1444
1445 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001446 """Set this branch's patchset. If patchset=0, clears the patchset."""
1447 assert self.GetBranch()
1448 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001449 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001450 else:
1451 self.patchset = int(patchset)
1452 self._GitSetBranchConfigValue(
1453 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001455 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001456 """Set this branch's issue. If issue isn't given, clears the issue."""
1457 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001459 issue = int(issue)
1460 self._GitSetBranchConfigValue(
1461 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001462 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001463 codereview_server = self._codereview_impl.GetCodereviewServer()
1464 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001465 self._GitSetBranchConfigValue(
1466 self._codereview_impl.CodereviewServerConfigKey(),
1467 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 else:
tandrii5d48c322016-08-18 16:19:37 -07001469 # Reset all of these just to be clean.
1470 reset_suffixes = [
1471 'last-upload-hash',
1472 self._codereview_impl.IssueConfigKey(),
1473 self._codereview_impl.PatchsetConfigKey(),
1474 self._codereview_impl.CodereviewServerConfigKey(),
1475 ] + self._PostUnsetIssueProperties()
1476 for prop in reset_suffixes:
1477 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001478 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001479 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480
dnjba1b0f32016-09-02 12:37:42 -07001481 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001482 if not self.GitSanityChecks(upstream_branch):
1483 DieWithError('\nGit sanity check failure')
1484
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001485 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001486 if not root:
1487 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001488 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001489
1490 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001491 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001492 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001493 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001494 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001495 except subprocess2.CalledProcessError:
1496 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001497 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001498 'This branch probably doesn\'t exist anymore. To reset the\n'
1499 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001500 ' git branch --set-upstream-to origin/master %s\n'
1501 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001502 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001503
maruel@chromium.org52424302012-08-29 15:14:30 +00001504 issue = self.GetIssue()
1505 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001506 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001507 description = self.GetDescription()
1508 else:
1509 # If the change was never uploaded, use the log messages of all commits
1510 # up to the branch point, as git cl upload will prefill the description
1511 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001512 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1513 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001514
1515 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001516 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001517 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001518 name,
1519 description,
1520 absroot,
1521 files,
1522 issue,
1523 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001524 author,
1525 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001526
dsansomee2d6fd92016-09-08 00:10:47 -07001527 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001528 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001529 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001530 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001531
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001532 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1533 """Sets the description for this CL remotely.
1534
1535 You can get description_lines and footers with GetDescriptionFooters.
1536
1537 Args:
1538 description_lines (list(str)) - List of CL description lines without
1539 newline characters.
1540 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1541 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1542 `List-Of-Tokens`). It will be case-normalized so that each token is
1543 title-cased.
1544 """
1545 new_description = '\n'.join(description_lines)
1546 if footers:
1547 new_description += '\n'
1548 for k, v in footers:
1549 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1550 if not git_footers.FOOTER_PATTERN.match(foot):
1551 raise ValueError('Invalid footer %r' % foot)
1552 new_description += foot + '\n'
1553 self.UpdateDescription(new_description, force)
1554
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001555 def RunHook(self, committing, may_prompt, verbose, change):
1556 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1557 try:
1558 return presubmit_support.DoPresubmitChecks(change, committing,
1559 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1560 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001561 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1562 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001563 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001564 DieWithError(
1565 ('%s\nMaybe your depot_tools is out of date?\n'
1566 'If all fails, contact maruel@') % e)
1567
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001568 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1569 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001570 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1571 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001572 else:
1573 # Assume url.
1574 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1575 urlparse.urlparse(issue_arg))
1576 if not parsed_issue_arg or not parsed_issue_arg.valid:
1577 DieWithError('Failed to parse issue argument "%s". '
1578 'Must be an issue number or a valid URL.' % issue_arg)
1579 return self._codereview_impl.CMDPatchWithParsedIssue(
1580 parsed_issue_arg, reject, nocommit, directory)
1581
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001582 def CMDUpload(self, options, git_diff_args, orig_args):
1583 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001584 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001585 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001586 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001587 else:
1588 if self.GetBranch() is None:
1589 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1590
1591 # Default to diffing against common ancestor of upstream branch
1592 base_branch = self.GetCommonAncestorWithUpstream()
1593 git_diff_args = [base_branch, 'HEAD']
1594
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001595 # Fast best-effort checks to abort before running potentially
1596 # expensive hooks if uploading is likely to fail anyway. Passing these
1597 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001598 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001599 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001600
1601 # Apply watchlists on upload.
1602 change = self.GetChange(base_branch, None)
1603 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1604 files = [f.LocalPath() for f in change.AffectedFiles()]
1605 if not options.bypass_watchlists:
1606 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1607
1608 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001609 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001610 # Set the reviewer list now so that presubmit checks can access it.
1611 change_description = ChangeDescription(change.FullDescriptionText())
1612 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001613 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001614 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001615 change)
1616 change.SetDescriptionText(change_description.description)
1617 hook_results = self.RunHook(committing=False,
1618 may_prompt=not options.force,
1619 verbose=options.verbose,
1620 change=change)
1621 if not hook_results.should_continue():
1622 return 1
1623 if not options.reviewers and hook_results.reviewers:
1624 options.reviewers = hook_results.reviewers.split(',')
1625
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001626 # TODO(tandrii): Checking local patchset against remote patchset is only
1627 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1628 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001629 latest_patchset = self.GetMostRecentPatchset()
1630 local_patchset = self.GetPatchset()
1631 if (latest_patchset and local_patchset and
1632 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001633 print('The last upload made from this repository was patchset #%d but '
1634 'the most recent patchset on the server is #%d.'
1635 % (local_patchset, latest_patchset))
1636 print('Uploading will still work, but if you\'ve uploaded to this '
1637 'issue from another machine or branch the patch you\'re '
1638 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001639 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001640
1641 print_stats(options.similarity, options.find_copies, git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001642 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001643 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001644 if options.use_commit_queue:
1645 self.SetCQState(_CQState.COMMIT)
1646 elif options.cq_dry_run:
1647 self.SetCQState(_CQState.DRY_RUN)
1648
tandrii5d48c322016-08-18 16:19:37 -07001649 _git_set_branch_config_value('last-upload-hash',
1650 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001651 # Run post upload hooks, if specified.
1652 if settings.GetRunPostUploadHook():
1653 presubmit_support.DoPostUploadExecuter(
1654 change,
1655 self,
1656 settings.GetRoot(),
1657 options.verbose,
1658 sys.stdout)
1659
1660 # Upload all dependencies if specified.
1661 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001662 print()
1663 print('--dependencies has been specified.')
1664 print('All dependent local branches will be re-uploaded.')
1665 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001666 # Remove the dependencies flag from args so that we do not end up in a
1667 # loop.
1668 orig_args.remove('--dependencies')
1669 ret = upload_branch_deps(self, orig_args)
1670 return ret
1671
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001672 def SetCQState(self, new_state):
1673 """Update the CQ state for latest patchset.
1674
1675 Issue must have been already uploaded and known.
1676 """
1677 assert new_state in _CQState.ALL_STATES
1678 assert self.GetIssue()
1679 return self._codereview_impl.SetCQState(new_state)
1680
qyearsley1fdfcb62016-10-24 13:22:03 -07001681 def TriggerDryRun(self):
1682 """Triggers a dry run and prints a warning on failure."""
1683 # TODO(qyearsley): Either re-use this method in CMDset_commit
1684 # and CMDupload, or change CMDtry to trigger dry runs with
1685 # just SetCQState, and catch keyboard interrupt and other
1686 # errors in that method.
1687 try:
1688 self.SetCQState(_CQState.DRY_RUN)
1689 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1690 return 0
1691 except KeyboardInterrupt:
1692 raise
1693 except:
1694 print('WARNING: failed to trigger CQ Dry Run.\n'
1695 'Either:\n'
1696 ' * your project has no CQ\n'
1697 ' * you don\'t have permission to trigger Dry Run\n'
1698 ' * bug in this code (see stack trace below).\n'
1699 'Consider specifying which bots to trigger manually '
1700 'or asking your project owners for permissions '
1701 'or contacting Chrome Infrastructure team at '
1702 'https://www.chromium.org/infra\n\n')
1703 # Still raise exception so that stack trace is printed.
1704 raise
1705
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001706 # Forward methods to codereview specific implementation.
1707
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001708 def AddComment(self, message):
1709 return self._codereview_impl.AddComment(message)
1710
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001711 def GetCommentsSummary(self):
1712 """Returns list of _CommentSummary for each comment.
1713
1714 Note: comments per file or per line are not included,
1715 only top-level comments are returned.
1716 """
1717 return self._codereview_impl.GetCommentsSummary()
1718
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001719 def CloseIssue(self):
1720 return self._codereview_impl.CloseIssue()
1721
1722 def GetStatus(self):
1723 return self._codereview_impl.GetStatus()
1724
1725 def GetCodereviewServer(self):
1726 return self._codereview_impl.GetCodereviewServer()
1727
tandriide281ae2016-10-12 06:02:30 -07001728 def GetIssueOwner(self):
1729 """Get owner from codereview, which may differ from this checkout."""
1730 return self._codereview_impl.GetIssueOwner()
1731
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001732 def GetMostRecentPatchset(self):
1733 return self._codereview_impl.GetMostRecentPatchset()
1734
tandriide281ae2016-10-12 06:02:30 -07001735 def CannotTriggerTryJobReason(self):
1736 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1737 return self._codereview_impl.CannotTriggerTryJobReason()
1738
tandrii8c5a3532016-11-04 07:52:02 -07001739 def GetTryjobProperties(self, patchset=None):
1740 """Returns dictionary of properties to launch tryjob."""
1741 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1742
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001743 def __getattr__(self, attr):
1744 # This is because lots of untested code accesses Rietveld-specific stuff
1745 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001746 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001747 # Note that child method defines __getattr__ as well, and forwards it here,
1748 # because _RietveldChangelistImpl is not cleaned up yet, and given
1749 # deprecation of Rietveld, it should probably be just removed.
1750 # Until that time, avoid infinite recursion by bypassing __getattr__
1751 # of implementation class.
1752 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753
1754
1755class _ChangelistCodereviewBase(object):
1756 """Abstract base class encapsulating codereview specifics of a changelist."""
1757 def __init__(self, changelist):
1758 self._changelist = changelist # instance of Changelist
1759
1760 def __getattr__(self, attr):
1761 # Forward methods to changelist.
1762 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1763 # _RietveldChangelistImpl to avoid this hack?
1764 return getattr(self._changelist, attr)
1765
1766 def GetStatus(self):
1767 """Apply a rough heuristic to give a simple summary of an issue's review
1768 or CQ status, assuming adherence to a common workflow.
1769
1770 Returns None if no issue for this branch, or specific string keywords.
1771 """
1772 raise NotImplementedError()
1773
1774 def GetCodereviewServer(self):
1775 """Returns server URL without end slash, like "https://codereview.com"."""
1776 raise NotImplementedError()
1777
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001778 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001779 """Fetches and returns description from the codereview server."""
1780 raise NotImplementedError()
1781
tandrii5d48c322016-08-18 16:19:37 -07001782 @classmethod
1783 def IssueConfigKey(cls):
1784 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001785 raise NotImplementedError()
1786
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001787 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001788 def PatchsetConfigKey(cls):
1789 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001790 raise NotImplementedError()
1791
tandrii5d48c322016-08-18 16:19:37 -07001792 @classmethod
1793 def CodereviewServerConfigKey(cls):
1794 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001795 raise NotImplementedError()
1796
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001797 def _PostUnsetIssueProperties(self):
1798 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001799 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001800
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001801 def GetRieveldObjForPresubmit(self):
1802 # This is an unfortunate Rietveld-embeddedness in presubmit.
1803 # For non-Rietveld codereviews, this probably should return a dummy object.
1804 raise NotImplementedError()
1805
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001806 def GetGerritObjForPresubmit(self):
1807 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1808 return None
1809
dsansomee2d6fd92016-09-08 00:10:47 -07001810 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001811 """Update the description on codereview site."""
1812 raise NotImplementedError()
1813
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001814 def AddComment(self, message):
1815 """Posts a comment to the codereview site."""
1816 raise NotImplementedError()
1817
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001818 def GetCommentsSummary(self):
1819 raise NotImplementedError()
1820
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001821 def CloseIssue(self):
1822 """Closes the issue."""
1823 raise NotImplementedError()
1824
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001825 def GetMostRecentPatchset(self):
1826 """Returns the most recent patchset number from the codereview site."""
1827 raise NotImplementedError()
1828
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001829 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1830 directory):
1831 """Fetches and applies the issue.
1832
1833 Arguments:
1834 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1835 reject: if True, reject the failed patch instead of switching to 3-way
1836 merge. Rietveld only.
1837 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1838 only.
1839 directory: switch to directory before applying the patch. Rietveld only.
1840 """
1841 raise NotImplementedError()
1842
1843 @staticmethod
1844 def ParseIssueURL(parsed_url):
1845 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1846 failed."""
1847 raise NotImplementedError()
1848
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001849 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001850 """Best effort check that user is authenticated with codereview server.
1851
1852 Arguments:
1853 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001854 refresh: whether to attempt to refresh credentials. Ignored if not
1855 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001856 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001857 raise NotImplementedError()
1858
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001859 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001860 """Best effort check that uploading isn't supposed to fail for predictable
1861 reasons.
1862
1863 This method should raise informative exception if uploading shouldn't
1864 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001865
1866 Arguments:
1867 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001868 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001869 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001870
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001871 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001872 """Uploads a change to codereview."""
1873 raise NotImplementedError()
1874
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001875 def SetCQState(self, new_state):
1876 """Update the CQ state for latest patchset.
1877
1878 Issue must have been already uploaded and known.
1879 """
1880 raise NotImplementedError()
1881
tandriie113dfd2016-10-11 10:20:12 -07001882 def CannotTriggerTryJobReason(self):
1883 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1884 raise NotImplementedError()
1885
tandriide281ae2016-10-12 06:02:30 -07001886 def GetIssueOwner(self):
1887 raise NotImplementedError()
1888
tandrii8c5a3532016-11-04 07:52:02 -07001889 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001890 raise NotImplementedError()
1891
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001892
1893class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001894 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001895 super(_RietveldChangelistImpl, self).__init__(changelist)
1896 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001897 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001898 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001899
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001900 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001901 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001902 self._props = None
1903 self._rpc_server = None
1904
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001905 def GetCodereviewServer(self):
1906 if not self._rietveld_server:
1907 # If we're on a branch then get the server potentially associated
1908 # with that branch.
1909 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001910 self._rietveld_server = gclient_utils.UpgradeToHttps(
1911 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001912 if not self._rietveld_server:
1913 self._rietveld_server = settings.GetDefaultServerUrl()
1914 return self._rietveld_server
1915
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001916 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001917 """Best effort check that user is authenticated with Rietveld server."""
1918 if self._auth_config.use_oauth2:
1919 authenticator = auth.get_authenticator_for_host(
1920 self.GetCodereviewServer(), self._auth_config)
1921 if not authenticator.has_cached_credentials():
1922 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001923 if refresh:
1924 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001925
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001926 def EnsureCanUploadPatchset(self, force):
1927 # No checks for Rietveld because we are deprecating Rietveld.
1928 pass
1929
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001930 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001931 issue = self.GetIssue()
1932 assert issue
1933 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001934 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001935 except urllib2.HTTPError as e:
1936 if e.code == 404:
1937 DieWithError(
1938 ('\nWhile fetching the description for issue %d, received a '
1939 '404 (not found)\n'
1940 'error. It is likely that you deleted this '
1941 'issue on the server. If this is the\n'
1942 'case, please run\n\n'
1943 ' git cl issue 0\n\n'
1944 'to clear the association with the deleted issue. Then run '
1945 'this command again.') % issue)
1946 else:
1947 DieWithError(
1948 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1949 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001950 print('Warning: Failed to retrieve CL description due to network '
1951 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001952 return ''
1953
1954 def GetMostRecentPatchset(self):
1955 return self.GetIssueProperties()['patchsets'][-1]
1956
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001957 def GetIssueProperties(self):
1958 if self._props is None:
1959 issue = self.GetIssue()
1960 if not issue:
1961 self._props = {}
1962 else:
1963 self._props = self.RpcServer().get_issue_properties(issue, True)
1964 return self._props
1965
tandriie113dfd2016-10-11 10:20:12 -07001966 def CannotTriggerTryJobReason(self):
1967 props = self.GetIssueProperties()
1968 if not props:
1969 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1970 if props.get('closed'):
1971 return 'CL %s is closed' % self.GetIssue()
1972 if props.get('private'):
1973 return 'CL %s is private' % self.GetIssue()
1974 return None
1975
tandrii8c5a3532016-11-04 07:52:02 -07001976 def GetTryjobProperties(self, patchset=None):
1977 """Returns dictionary of properties to launch tryjob."""
1978 project = (self.GetIssueProperties() or {}).get('project')
1979 return {
1980 'issue': self.GetIssue(),
1981 'patch_project': project,
1982 'patch_storage': 'rietveld',
1983 'patchset': patchset or self.GetPatchset(),
1984 'rietveld': self.GetCodereviewServer(),
1985 }
1986
tandriide281ae2016-10-12 06:02:30 -07001987 def GetIssueOwner(self):
1988 return (self.GetIssueProperties() or {}).get('owner_email')
1989
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001990 def AddComment(self, message):
1991 return self.RpcServer().add_comment(self.GetIssue(), message)
1992
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001993 def GetCommentsSummary(self):
1994 summary = []
1995 for message in self.GetIssueProperties().get('messages', []):
1996 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
1997 summary.append(_CommentSummary(
1998 date=date,
1999 disapproval=bool(message['disapproval']),
2000 approval=bool(message['approval']),
2001 sender=message['sender'],
2002 message=message['text'],
2003 ))
2004 return summary
2005
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002006 def GetStatus(self):
2007 """Apply a rough heuristic to give a simple summary of an issue's review
2008 or CQ status, assuming adherence to a common workflow.
2009
2010 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002011 * 'error' - error from review tool (including deleted issues)
2012 * 'unsent' - not sent for review
2013 * 'waiting' - waiting for review
2014 * 'reply' - waiting for owner to reply to review
2015 * 'not lgtm' - Code-Review label has been set negatively
2016 * 'lgtm' - LGTM from at least one approved reviewer
2017 * 'commit' - in the commit queue
2018 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002019 """
2020 if not self.GetIssue():
2021 return None
2022
2023 try:
2024 props = self.GetIssueProperties()
2025 except urllib2.HTTPError:
2026 return 'error'
2027
2028 if props.get('closed'):
2029 # Issue is closed.
2030 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002031 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002032 # Issue is in the commit queue.
2033 return 'commit'
2034
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002035 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002036 if not messages:
2037 # No message was sent.
2038 return 'unsent'
2039
2040 if get_approving_reviewers(props):
2041 return 'lgtm'
2042 elif get_approving_reviewers(props, disapproval=True):
2043 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002044
tandrii9d2c7a32016-06-22 03:42:45 -07002045 # Skip CQ messages that don't require owner's action.
2046 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2047 if 'Dry run:' in messages[-1]['text']:
2048 messages.pop()
2049 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2050 # This message always follows prior messages from CQ,
2051 # so skip this too.
2052 messages.pop()
2053 else:
2054 # This is probably a CQ messages warranting user attention.
2055 break
2056
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002057 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002058 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002059 return 'reply'
2060 return 'waiting'
2061
dsansomee2d6fd92016-09-08 00:10:47 -07002062 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002063 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002064
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002065 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002066 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002067
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002068 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002069 return self.SetFlags({flag: value})
2070
2071 def SetFlags(self, flags):
2072 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002073 """
phajdan.jr68598232016-08-10 03:28:28 -07002074 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002075 try:
tandrii4b233bd2016-07-06 03:50:29 -07002076 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002077 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002078 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002079 if e.code == 404:
2080 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2081 if e.code == 403:
2082 DieWithError(
2083 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002084 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002085 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002086
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002087 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002088 """Returns an upload.RpcServer() to access this review's rietveld instance.
2089 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002090 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002091 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002092 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002093 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002094 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002095
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002096 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002097 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002098 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002099
tandrii5d48c322016-08-18 16:19:37 -07002100 @classmethod
2101 def PatchsetConfigKey(cls):
2102 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002103
tandrii5d48c322016-08-18 16:19:37 -07002104 @classmethod
2105 def CodereviewServerConfigKey(cls):
2106 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002107
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002108 def GetRieveldObjForPresubmit(self):
2109 return self.RpcServer()
2110
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002111 def SetCQState(self, new_state):
2112 props = self.GetIssueProperties()
2113 if props.get('private'):
2114 DieWithError('Cannot set-commit on private issue')
2115
2116 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002117 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002118 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002119 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002120 else:
tandrii4b233bd2016-07-06 03:50:29 -07002121 assert new_state == _CQState.DRY_RUN
2122 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002123
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002124 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2125 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002126 # PatchIssue should never be called with a dirty tree. It is up to the
2127 # caller to check this, but just in case we assert here since the
2128 # consequences of the caller not checking this could be dire.
2129 assert(not git_common.is_dirty_git_tree('apply'))
2130 assert(parsed_issue_arg.valid)
2131 self._changelist.issue = parsed_issue_arg.issue
2132 if parsed_issue_arg.hostname:
2133 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2134
skobes6468b902016-10-24 08:45:10 -07002135 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2136 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2137 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002138 try:
skobes6468b902016-10-24 08:45:10 -07002139 scm_obj.apply_patch(patchset_object)
2140 except Exception as e:
2141 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002142 return 1
2143
2144 # If we had an issue, commit the current state and register the issue.
2145 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002146 self.SetIssue(self.GetIssue())
2147 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002148 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2149 'patch from issue %(i)s at patchset '
2150 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2151 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002152 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002153 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002154 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002155 return 0
2156
2157 @staticmethod
2158 def ParseIssueURL(parsed_url):
2159 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2160 return None
wychen3c1c1722016-08-04 11:46:36 -07002161 # Rietveld patch: https://domain/<number>/#ps<patchset>
2162 match = re.match(r'/(\d+)/$', parsed_url.path)
2163 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2164 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002165 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002166 issue=int(match.group(1)),
2167 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002168 hostname=parsed_url.netloc,
2169 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002170 # Typical url: https://domain/<issue_number>[/[other]]
2171 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2172 if match:
skobes6468b902016-10-24 08:45:10 -07002173 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002174 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002175 hostname=parsed_url.netloc,
2176 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002177 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2178 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2179 if match:
skobes6468b902016-10-24 08:45:10 -07002180 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002181 issue=int(match.group(1)),
2182 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002183 hostname=parsed_url.netloc,
2184 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002185 return None
2186
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002187 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002188 """Upload the patch to Rietveld."""
2189 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2190 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002191 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2192 if options.emulate_svn_auto_props:
2193 upload_args.append('--emulate_svn_auto_props')
2194
2195 change_desc = None
2196
2197 if options.email is not None:
2198 upload_args.extend(['--email', options.email])
2199
2200 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002201 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002202 upload_args.extend(['--title', options.title])
2203 if options.message:
2204 upload_args.extend(['--message', options.message])
2205 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002206 print('This branch is associated with issue %s. '
2207 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002208 else:
nodirca166002016-06-27 10:59:51 -07002209 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002210 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002211 if options.message:
2212 message = options.message
2213 else:
2214 message = CreateDescriptionFromLog(args)
2215 if options.title:
2216 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002217 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002218 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002219 change_desc.update_reviewers(options.reviewers, options.tbrs,
2220 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002221 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002222 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002223
2224 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002225 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002226 return 1
2227
2228 upload_args.extend(['--message', change_desc.description])
2229 if change_desc.get_reviewers():
2230 upload_args.append('--reviewers=%s' % ','.join(
2231 change_desc.get_reviewers()))
2232 if options.send_mail:
2233 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002234 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002235 upload_args.append('--send_mail')
2236
2237 # We check this before applying rietveld.private assuming that in
2238 # rietveld.cc only addresses which we can send private CLs to are listed
2239 # if rietveld.private is set, and so we should ignore rietveld.cc only
2240 # when --private is specified explicitly on the command line.
2241 if options.private:
2242 logging.warn('rietveld.cc is ignored since private flag is specified. '
2243 'You need to review and add them manually if necessary.')
2244 cc = self.GetCCListWithoutDefault()
2245 else:
2246 cc = self.GetCCList()
2247 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002248 if change_desc.get_cced():
2249 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002250 if cc:
2251 upload_args.extend(['--cc', cc])
2252
2253 if options.private or settings.GetDefaultPrivateFlag() == "True":
2254 upload_args.append('--private')
2255
2256 upload_args.extend(['--git_similarity', str(options.similarity)])
2257 if not options.find_copies:
2258 upload_args.extend(['--git_no_find_copies'])
2259
2260 # Include the upstream repo's URL in the change -- this is useful for
2261 # projects that have their source spread across multiple repos.
2262 remote_url = self.GetGitBaseUrlFromConfig()
2263 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002264 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2265 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2266 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002267 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002268 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002269 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002270 if target_ref:
2271 upload_args.extend(['--target_ref', target_ref])
2272
2273 # Look for dependent patchsets. See crbug.com/480453 for more details.
2274 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2275 upstream_branch = ShortBranchName(upstream_branch)
2276 if remote is '.':
2277 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002278 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002279 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002280 print()
2281 print('Skipping dependency patchset upload because git config '
2282 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2283 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002284 else:
2285 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002286 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002287 auth_config=auth_config)
2288 branch_cl_issue_url = branch_cl.GetIssueURL()
2289 branch_cl_issue = branch_cl.GetIssue()
2290 branch_cl_patchset = branch_cl.GetPatchset()
2291 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2292 upload_args.extend(
2293 ['--depends_on_patchset', '%s:%s' % (
2294 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002295 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002296 '\n'
2297 'The current branch (%s) is tracking a local branch (%s) with '
2298 'an associated CL.\n'
2299 'Adding %s/#ps%s as a dependency patchset.\n'
2300 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2301 branch_cl_patchset))
2302
2303 project = settings.GetProject()
2304 if project:
2305 upload_args.extend(['--project', project])
2306
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002307 try:
2308 upload_args = ['upload'] + upload_args + args
2309 logging.info('upload.RealMain(%s)', upload_args)
2310 issue, patchset = upload.RealMain(upload_args)
2311 issue = int(issue)
2312 patchset = int(patchset)
2313 except KeyboardInterrupt:
2314 sys.exit(1)
2315 except:
2316 # If we got an exception after the user typed a description for their
2317 # change, back up the description before re-raising.
2318 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002319 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002320 raise
2321
2322 if not self.GetIssue():
2323 self.SetIssue(issue)
2324 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002325 return 0
2326
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002327
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002328class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002329 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002330 # auth_config is Rietveld thing, kept here to preserve interface only.
2331 super(_GerritChangelistImpl, self).__init__(changelist)
2332 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002333 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002334 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002335 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002336 # Map from change number (issue) to its detail cache.
2337 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002338
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002339 if codereview_host is not None:
2340 assert not codereview_host.startswith('https://'), codereview_host
2341 self._gerrit_host = codereview_host
2342 self._gerrit_server = 'https://%s' % codereview_host
2343
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002344 def _GetGerritHost(self):
2345 # Lazy load of configs.
2346 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002347 if self._gerrit_host and '.' not in self._gerrit_host:
2348 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2349 # This happens for internal stuff http://crbug.com/614312.
2350 parsed = urlparse.urlparse(self.GetRemoteUrl())
2351 if parsed.scheme == 'sso':
2352 print('WARNING: using non https URLs for remote is likely broken\n'
2353 ' Your current remote is: %s' % self.GetRemoteUrl())
2354 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2355 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002356 return self._gerrit_host
2357
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002358 def _GetGitHost(self):
2359 """Returns git host to be used when uploading change to Gerrit."""
2360 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2361
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002362 def GetCodereviewServer(self):
2363 if not self._gerrit_server:
2364 # If we're on a branch then get the server potentially associated
2365 # with that branch.
2366 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002367 self._gerrit_server = self._GitGetBranchConfigValue(
2368 self.CodereviewServerConfigKey())
2369 if self._gerrit_server:
2370 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002371 if not self._gerrit_server:
2372 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2373 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002374 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002375 parts[0] = parts[0] + '-review'
2376 self._gerrit_host = '.'.join(parts)
2377 self._gerrit_server = 'https://%s' % self._gerrit_host
2378 return self._gerrit_server
2379
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002380 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002381 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002382 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002383
tandrii5d48c322016-08-18 16:19:37 -07002384 @classmethod
2385 def PatchsetConfigKey(cls):
2386 return 'gerritpatchset'
2387
2388 @classmethod
2389 def CodereviewServerConfigKey(cls):
2390 return 'gerritserver'
2391
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002392 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002393 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002394 if settings.GetGerritSkipEnsureAuthenticated():
2395 # For projects with unusual authentication schemes.
2396 # See http://crbug.com/603378.
2397 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002398 # Lazy-loader to identify Gerrit and Git hosts.
2399 if gerrit_util.GceAuthenticator.is_gce():
2400 return
2401 self.GetCodereviewServer()
2402 git_host = self._GetGitHost()
2403 assert self._gerrit_server and self._gerrit_host
2404 cookie_auth = gerrit_util.CookiesAuthenticator()
2405
2406 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2407 git_auth = cookie_auth.get_auth_header(git_host)
2408 if gerrit_auth and git_auth:
2409 if gerrit_auth == git_auth:
2410 return
2411 print((
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002412 'WARNING: you have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002413 ' %s\n'
2414 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002415 ' Consider running the following command:\n'
2416 ' git cl creds-check\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002417 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002418 (git_host, self._gerrit_host,
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002419 cookie_auth.get_new_password_message(git_host)))
2420 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002421 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002422 return
2423 else:
2424 missing = (
2425 [] if gerrit_auth else [self._gerrit_host] +
2426 [] if git_auth else [git_host])
2427 DieWithError('Credentials for the following hosts are required:\n'
2428 ' %s\n'
2429 'These are read from %s (or legacy %s)\n'
2430 '%s' % (
2431 '\n '.join(missing),
2432 cookie_auth.get_gitcookies_path(),
2433 cookie_auth.get_netrc_path(),
2434 cookie_auth.get_new_password_message(git_host)))
2435
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002436 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002437 if not self.GetIssue():
2438 return
2439
2440 # Warm change details cache now to avoid RPCs later, reducing latency for
2441 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002442 self._GetChangeDetail(
2443 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002444
2445 status = self._GetChangeDetail()['status']
2446 if status in ('MERGED', 'ABANDONED'):
2447 DieWithError('Change %s has been %s, new uploads are not allowed' %
2448 (self.GetIssueURL(),
2449 'submitted' if status == 'MERGED' else 'abandoned'))
2450
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002451 if gerrit_util.GceAuthenticator.is_gce():
2452 return
2453 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2454 self._GetGerritHost())
2455 if self.GetIssueOwner() == cookies_user:
2456 return
2457 logging.debug('change %s owner is %s, cookies user is %s',
2458 self.GetIssue(), self.GetIssueOwner(), cookies_user)
2459 # Maybe user has linked accounts or smth like that,
2460 # so ask what Gerrit thinks of this user.
2461 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2462 if details['email'] == self.GetIssueOwner():
2463 return
2464 if not force:
2465 print('WARNING: change %s is owned by %s, but you authenticate to Gerrit '
2466 'as %s.\n'
2467 'Uploading may fail due to lack of permissions.' %
2468 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2469 confirm_or_exit(action='upload')
2470
2471
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002472 def _PostUnsetIssueProperties(self):
2473 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002474 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002475
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002476 def GetRieveldObjForPresubmit(self):
2477 class ThisIsNotRietveldIssue(object):
2478 def __nonzero__(self):
2479 # This is a hack to make presubmit_support think that rietveld is not
2480 # defined, yet still ensure that calls directly result in a decent
2481 # exception message below.
2482 return False
2483
2484 def __getattr__(self, attr):
2485 print(
2486 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2487 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2488 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2489 'or use Rietveld for codereview.\n'
2490 'See also http://crbug.com/579160.' % attr)
2491 raise NotImplementedError()
2492 return ThisIsNotRietveldIssue()
2493
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002494 def GetGerritObjForPresubmit(self):
2495 return presubmit_support.GerritAccessor(self._GetGerritHost())
2496
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002497 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002498 """Apply a rough heuristic to give a simple summary of an issue's review
2499 or CQ status, assuming adherence to a common workflow.
2500
2501 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002502 * 'error' - error from review tool (including deleted issues)
2503 * 'unsent' - no reviewers added
2504 * 'waiting' - waiting for review
2505 * 'reply' - waiting for uploader to reply to review
2506 * 'lgtm' - Code-Review label has been set
2507 * 'commit' - in the commit queue
2508 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002509 """
2510 if not self.GetIssue():
2511 return None
2512
2513 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002514 data = self._GetChangeDetail([
2515 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002516 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002517 return 'error'
2518
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002519 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002520 return 'closed'
2521
Aaron Gable9ab38c62017-04-06 14:36:33 -07002522 if data['labels'].get('Commit-Queue', {}).get('approved'):
2523 # The section will have an "approved" subsection if anyone has voted
2524 # the maximum value on the label.
2525 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002526
Aaron Gable9ab38c62017-04-06 14:36:33 -07002527 if data['labels'].get('Code-Review', {}).get('approved'):
2528 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002529
2530 if not data.get('reviewers', {}).get('REVIEWER', []):
2531 return 'unsent'
2532
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002533 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002534 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2535 last_message_author = messages.pop().get('author', {})
2536 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002537 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2538 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002539 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002540 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002541 if last_message_author.get('_account_id') == owner:
2542 # Most recent message was by owner.
2543 return 'waiting'
2544 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002545 # Some reply from non-owner.
2546 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002547
2548 # Somehow there are no messages even though there are reviewers.
2549 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002550
2551 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002552 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002553 return data['revisions'][data['current_revision']]['_number']
2554
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002555 def FetchDescription(self, force=False):
2556 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2557 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002558 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002559 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002560
dsansomee2d6fd92016-09-08 00:10:47 -07002561 def UpdateDescriptionRemote(self, description, force=False):
2562 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2563 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002564 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002565 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002566 'unpublished edit. Either publish the edit in the Gerrit web UI '
2567 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002568
2569 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2570 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002571 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002572 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002573
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002574 def AddComment(self, message):
2575 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2576 msg=message)
2577
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002578 def GetCommentsSummary(self):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002579 # DETAILED_ACCOUNTS is to get emails in accounts.
2580 data = self._GetChangeDetail(options=['MESSAGES', 'DETAILED_ACCOUNTS'])
2581 summary = []
2582 for msg in data.get('messages', []):
2583 # Gerrit spits out nanoseconds.
2584 assert len(msg['date'].split('.')[-1]) == 9
2585 date = datetime.datetime.strptime(msg['date'][:-3],
2586 '%Y-%m-%d %H:%M:%S.%f')
2587 summary.append(_CommentSummary(
2588 date=date,
2589 message=msg['message'],
2590 sender=msg['author']['email'],
2591 # These could be inferred from the text messages and correlated with
2592 # Code-Review label maximum, however this is not reliable.
2593 # Leaving as is until the need arises.
2594 approval=False,
2595 disapproval=False,
2596 ))
2597 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002598
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002599 def CloseIssue(self):
2600 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2601
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002602 def SubmitIssue(self, wait_for_merge=True):
2603 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2604 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002605
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002606 def _GetChangeDetail(self, options=None, issue=None,
2607 no_cache=False):
2608 """Returns details of the issue by querying Gerrit and caching results.
2609
2610 If fresh data is needed, set no_cache=True which will clear cache and
2611 thus new data will be fetched from Gerrit.
2612 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002613 options = options or []
2614 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002615 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002616
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002617 # Optimization to avoid multiple RPCs:
2618 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2619 'CURRENT_COMMIT' not in options):
2620 options.append('CURRENT_COMMIT')
2621
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002622 # Normalize issue and options for consistent keys in cache.
2623 issue = str(issue)
2624 options = [o.upper() for o in options]
2625
2626 # Check in cache first unless no_cache is True.
2627 if no_cache:
2628 self._detail_cache.pop(issue, None)
2629 else:
2630 options_set = frozenset(options)
2631 for cached_options_set, data in self._detail_cache.get(issue, []):
2632 # Assumption: data fetched before with extra options is suitable
2633 # for return for a smaller set of options.
2634 # For example, if we cached data for
2635 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2636 # and request is for options=[CURRENT_REVISION],
2637 # THEN we can return prior cached data.
2638 if options_set.issubset(cached_options_set):
2639 return data
2640
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002641 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002642 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002643 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002644 except gerrit_util.GerritError as e:
2645 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002646 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002647 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002648
2649 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002650 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002651
agable32978d92016-11-01 12:55:02 -07002652 def _GetChangeCommit(self, issue=None):
2653 issue = issue or self.GetIssue()
2654 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002655 try:
2656 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2657 except gerrit_util.GerritError as e:
2658 if e.http_status == 404:
2659 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2660 raise
agable32978d92016-11-01 12:55:02 -07002661 return data
2662
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002663 def CMDLand(self, force, bypass_hooks, verbose):
2664 if git_common.is_dirty_git_tree('land'):
2665 return 1
tandriid60367b2016-06-22 05:25:12 -07002666 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2667 if u'Commit-Queue' in detail.get('labels', {}):
2668 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002669 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2670 'which can test and land changes for you. '
2671 'Are you sure you wish to bypass it?\n',
2672 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002673
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002674 differs = True
tandriic4344b52016-08-29 06:04:54 -07002675 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002676 # Note: git diff outputs nothing if there is no diff.
2677 if not last_upload or RunGit(['diff', last_upload]).strip():
2678 print('WARNING: some changes from local branch haven\'t been uploaded')
2679 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002680 if detail['current_revision'] == last_upload:
2681 differs = False
2682 else:
2683 print('WARNING: local branch contents differ from latest uploaded '
2684 'patchset')
2685 if differs:
2686 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002687 confirm_or_exit(
2688 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2689 action='submit')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002690 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2691 elif not bypass_hooks:
2692 hook_results = self.RunHook(
2693 committing=True,
2694 may_prompt=not force,
2695 verbose=verbose,
2696 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2697 if not hook_results.should_continue():
2698 return 1
2699
2700 self.SubmitIssue(wait_for_merge=True)
2701 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002702 links = self._GetChangeCommit().get('web_links', [])
2703 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002704 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002705 print('Landed as %s' % link.get('url'))
2706 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002707 return 0
2708
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002709 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2710 directory):
2711 assert not reject
2712 assert not nocommit
2713 assert not directory
2714 assert parsed_issue_arg.valid
2715
2716 self._changelist.issue = parsed_issue_arg.issue
2717
2718 if parsed_issue_arg.hostname:
2719 self._gerrit_host = parsed_issue_arg.hostname
2720 self._gerrit_server = 'https://%s' % self._gerrit_host
2721
tandriic2405f52016-10-10 08:13:15 -07002722 try:
2723 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002724 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002725 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002726
2727 if not parsed_issue_arg.patchset:
2728 # Use current revision by default.
2729 revision_info = detail['revisions'][detail['current_revision']]
2730 patchset = int(revision_info['_number'])
2731 else:
2732 patchset = parsed_issue_arg.patchset
2733 for revision_info in detail['revisions'].itervalues():
2734 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2735 break
2736 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002737 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002738 (parsed_issue_arg.patchset, self.GetIssue()))
2739
2740 fetch_info = revision_info['fetch']['http']
2741 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002742 self.SetIssue(self.GetIssue())
2743 self.SetPatchset(patchset)
Aaron Gabled343c632017-03-15 11:02:26 -07002744 RunGit(['cherry-pick', 'FETCH_HEAD'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002745 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002746 (self.GetIssue(), self.GetPatchset()))
2747 return 0
2748
2749 @staticmethod
2750 def ParseIssueURL(parsed_url):
2751 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2752 return None
2753 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2754 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2755 # Short urls like https://domain/<issue_number> can be used, but don't allow
2756 # specifying the patchset (you'd 404), but we allow that here.
2757 if parsed_url.path == '/':
2758 part = parsed_url.fragment
2759 else:
2760 part = parsed_url.path
2761 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2762 if match:
2763 return _ParsedIssueNumberArgument(
2764 issue=int(match.group(2)),
2765 patchset=int(match.group(4)) if match.group(4) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002766 hostname=parsed_url.netloc,
2767 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002768 return None
2769
tandrii16e0b4e2016-06-07 10:34:28 -07002770 def _GerritCommitMsgHookCheck(self, offer_removal):
2771 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2772 if not os.path.exists(hook):
2773 return
2774 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2775 # custom developer made one.
2776 data = gclient_utils.FileRead(hook)
2777 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2778 return
2779 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002780 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002781 'and may interfere with it in subtle ways.\n'
2782 'We recommend you remove the commit-msg hook.')
2783 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002784 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002785 gclient_utils.rm_file_or_tree(hook)
2786 print('Gerrit commit-msg hook removed.')
2787 else:
2788 print('OK, will keep Gerrit commit-msg hook in place.')
2789
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002790 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002791 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002792 if options.squash and options.no_squash:
2793 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002794
2795 if not options.squash and not options.no_squash:
2796 # Load default for user, repo, squash=true, in this order.
2797 options.squash = settings.GetSquashGerritUploads()
2798 elif options.no_squash:
2799 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002800
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002801 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002802 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002803
Aaron Gableb56ad332017-01-06 15:24:31 -08002804 # This may be None; default fallback value is determined in logic below.
2805 title = options.title
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002806 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002807
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002808 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002809 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002810 if self.GetIssue():
2811 # Try to get the message from a previous upload.
2812 message = self.GetDescription()
2813 if not message:
2814 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002815 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002816 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002817 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002818 if options.message:
2819 # For compatibility with Rietveld, if -m|--message is given on
2820 # command line, title should be the first line of that message,
2821 # which shouldn't be confused with CL description.
2822 default_title = options.message.strip().split()[0]
2823 else:
2824 default_title = RunGit(
2825 ['show', '-s', '--format=%s', 'HEAD']).strip()
Andrii Shyshkalove00a29b2017-04-10 14:48:28 +02002826 if options.force:
2827 title = default_title
2828 else:
2829 title = ask_for_data(
2830 'Title for patchset [%s]: ' % default_title) or default_title
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002831 if title == default_title:
2832 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002833 change_id = self._GetChangeDetail()['change_id']
2834 while True:
2835 footer_change_ids = git_footers.get_footer_change_id(message)
2836 if footer_change_ids == [change_id]:
2837 break
2838 if not footer_change_ids:
2839 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002840 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002841 continue
2842 # There is already a valid footer but with different or several ids.
2843 # Doing this automatically is non-trivial as we don't want to lose
2844 # existing other footers, yet we want to append just 1 desired
2845 # Change-Id. Thus, just create a new footer, but let user verify the
2846 # new description.
2847 message = '%s\n\nChange-Id: %s' % (message, change_id)
2848 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002849 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002850 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002851 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002852 'Please, check the proposed correction to the description, '
2853 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2854 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2855 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002856 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002857 if not options.force:
2858 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002859 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002860 message = change_desc.description
2861 if not message:
2862 DieWithError("Description is empty. Aborting...")
2863 # Continue the while loop.
2864 # Sanity check of this code - we should end up with proper message
2865 # footer.
2866 assert [change_id] == git_footers.get_footer_change_id(message)
2867 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002868 else: # if not self.GetIssue()
2869 if options.message:
2870 message = options.message
2871 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002872 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002873 if options.title:
2874 message = options.title + '\n\n' + message
2875 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002876
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002877 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002878 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002879 # On first upload, patchset title is always this string, while
2880 # --title flag gets converted to first line of message.
2881 title = 'Initial upload'
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002882 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002883 if not change_desc.description:
2884 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002885 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002886 if len(change_ids) > 1:
2887 DieWithError('too many Change-Id footers, at most 1 allowed.')
2888 if not change_ids:
2889 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002890 change_desc.set_description(git_footers.add_footer_change_id(
2891 change_desc.description,
2892 GenerateGerritChangeId(change_desc.description)))
2893 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002894 assert len(change_ids) == 1
2895 change_id = change_ids[0]
2896
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002897 if options.reviewers or options.tbrs or options.add_owners_to:
2898 change_desc.update_reviewers(options.reviewers, options.tbrs,
2899 options.add_owners_to, change)
2900
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002901 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002902 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2903 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002904 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2905 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002906 '-m', change_desc.description]).strip()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002907 else:
2908 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002909 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002910 if not change_desc.description:
2911 DieWithError("Description is empty. Aborting...")
2912
2913 if not git_footers.get_footer_change_id(change_desc.description):
2914 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002915 change_desc.set_description(
2916 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002917 if options.reviewers or options.tbrs or options.add_owners_to:
2918 change_desc.update_reviewers(options.reviewers, options.tbrs,
2919 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002920 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002921 # For no-squash mode, we assume the remote called "origin" is the one we
2922 # want. It is not worthwhile to support different workflows for
2923 # no-squash mode.
2924 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002925 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2926
2927 assert change_desc
2928 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2929 ref_to_push)]).splitlines()
2930 if len(commits) > 1:
2931 print('WARNING: This will upload %d commits. Run the following command '
2932 'to see which commits will be uploaded: ' % len(commits))
2933 print('git log %s..%s' % (parent, ref_to_push))
2934 print('You can also use `git squash-branch` to squash these into a '
2935 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002936 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002937
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002938 if options.reviewers or options.tbrs or options.add_owners_to:
2939 change_desc.update_reviewers(options.reviewers, options.tbrs,
2940 options.add_owners_to, change)
2941
2942 if options.send_mail:
2943 if not change_desc.get_reviewers():
2944 DieWithError('Must specify reviewers to send email.', change_desc)
2945
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002946 # Extra options that can be specified at push time. Doc:
2947 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002948 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002949 if change_desc.get_reviewers(tbr_only=True):
2950 print('Adding self-LGTM (Code-Review +1) because of TBRs')
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002951 refspec_opts.append('l=Code-Review+1')
tandrii99a72f22016-08-17 14:33:24 -07002952
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002953
2954 # TODO(tandrii): options.message should be posted as a comment
2955 # if --send-email is set on non-initial upload as Rietveld used to do it.
2956
Aaron Gable9b713dd2016-12-14 16:04:21 -08002957 if title:
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002958 if not re.match(r'^[\w ]+$', title):
2959 title = re.sub(r'[^\w ]', '', title)
2960 if not automatic_title:
2961 print('WARNING: Patchset title may only contain alphanumeric chars '
2962 'and spaces. You can edit it in the UI. '
2963 'See https://crbug.com/663787.\n'
2964 'Cleaned up title: %s' % title)
2965 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2966 # reverse on its side.
2967 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002968
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002969 # Never notify now because no one is on the review. Notify when we add
2970 # reviewers and CCs below.
2971 refspec_opts.append('notify=NONE')
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002972
agablec6787972016-09-09 16:13:34 -07002973 if options.private:
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002974 refspec_opts.append('draft')
agablec6787972016-09-09 16:13:34 -07002975
rmistry9eadede2016-09-19 11:22:43 -07002976 if options.topic:
2977 # Documentation on Gerrit topics is here:
2978 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002979 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002980
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002981 refspec_suffix = ''
2982 if refspec_opts:
2983 refspec_suffix = '%' + ','.join(refspec_opts)
2984 assert ' ' not in refspec_suffix, (
2985 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2986 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2987
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002988 try:
2989 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002990 ['git', 'push', self.GetRemoteUrl(), refspec],
2991 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002992 # Flush after every line: useful for seeing progress when running as
2993 # recipe.
2994 filter_fn=lambda _: sys.stdout.flush())
2995 except subprocess2.CalledProcessError:
2996 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002997 'for the reason of the failure.\n'
2998 'Hint: run command below to diangose common Git/Gerrit '
2999 'credential problems:\n'
3000 ' git cl creds-check\n',
3001 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003002
3003 if options.squash:
3004 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
3005 change_numbers = [m.group(1)
3006 for m in map(regex.match, push_stdout.splitlines())
3007 if m]
3008 if len(change_numbers) != 1:
3009 DieWithError(
3010 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003011 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003012 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003013 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003014
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003015 reviewers = sorted(change_desc.get_reviewers())
3016
tandrii88189772016-09-29 04:29:57 -07003017 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003018 if not options.private:
3019 cc = self.GetCCList().split(',')
3020 else:
3021 cc = []
tandrii88189772016-09-29 04:29:57 -07003022 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)