blob: 390f4b4f198040add24f8d292086ac62e1f995d2 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000032import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000033import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000034import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000035import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000036import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037
38try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080039 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000040except ImportError:
41 pass
42
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000043from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000044from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000046import auth
skobes6468b902016-10-24 08:45:10 -070047import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000048import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000049import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000050import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000051import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000052import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000053import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000054import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000056import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000057import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000058import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000060import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000062import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000063import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064import watchlists
65
tandrii7400cf02016-06-21 08:48:07 -070066__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067
tandrii9d2c7a32016-06-22 03:42:45 -070068COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070069DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080070POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000072REFS_THAT_ALIAS_TO_OTHER_REFS = {
73 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
74 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
75}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
thestig@chromium.org44202a22014-03-11 19:22:18 +000077# Valid extensions for files we want to lint.
78DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
79DEFAULT_LINT_IGNORE_REGEX = r"$^"
80
borenet6c0efe62016-10-19 08:13:29 -070081# Buildbucket master name prefix.
82MASTER_PREFIX = 'master.'
83
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000084# Shortcut since it quickly becomes redundant.
85Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000086
maruel@chromium.orgddd59412011-11-30 14:20:38 +000087# Initialized in main()
88settings = None
89
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010090# Used by tests/git_cl_test.py to add extra logging.
91# Inside the weirdly failing test, add this:
92# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
93# And scroll up to see the strack trace printed.
94_IS_BEING_TESTED = False
95
maruel@chromium.orgddd59412011-11-30 14:20:38 +000096
Christopher Lamf732cd52017-01-24 12:40:11 +110097def DieWithError(message, change_desc=None):
98 if change_desc:
99 SaveDescriptionBackup(change_desc)
100
vapiera7fbd5a2016-06-16 09:17:49 -0700101 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 sys.exit(1)
103
104
Christopher Lamf732cd52017-01-24 12:40:11 +1100105def SaveDescriptionBackup(change_desc):
106 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
107 print('\nError after CL description prompt -- saving description to %s\n' %
108 backup_path)
109 backup_file = open(backup_path, 'w')
110 backup_file.write(change_desc.description)
111 backup_file.close()
112
113
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000114def GetNoGitPagerEnv():
115 env = os.environ.copy()
116 # 'cat' is a magical git string that disables pagers on all platforms.
117 env['GIT_PAGER'] = 'cat'
118 return env
119
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000120
bsep@chromium.org627d9002016-04-29 00:00:52 +0000121def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000122 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000123 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000124 except subprocess2.CalledProcessError as e:
125 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000126 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000127 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000128 'Command "%s" failed.\n%s' % (
129 ' '.join(args), error_message or e.stdout or ''))
130 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
133def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000134 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000135 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000136
137
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000138def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000139 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700140 if suppress_stderr:
141 stderr = subprocess2.VOID
142 else:
143 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000144 try:
tandrii5d48c322016-08-18 16:19:37 -0700145 (out, _), code = subprocess2.communicate(['git'] + args,
146 env=GetNoGitPagerEnv(),
147 stdout=subprocess2.PIPE,
148 stderr=stderr)
149 return code, out
150 except subprocess2.CalledProcessError as e:
151 logging.debug('Failed running %s', args)
152 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000153
154
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000155def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000156 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000157 return RunGitWithCode(args, suppress_stderr=True)[1]
158
159
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000160def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000161 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000163 return (version.startswith(prefix) and
164 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000165
166
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000167def BranchExists(branch):
168 """Return True if specified branch exists."""
169 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
170 suppress_stderr=True)
171 return not code
172
173
tandrii2a16b952016-10-19 07:09:44 -0700174def time_sleep(seconds):
175 # Use this so that it can be mocked in tests without interfering with python
176 # system machinery.
177 import time # Local import to discourage others from importing time globally.
178 return time.sleep(seconds)
179
180
maruel@chromium.org90541732011-04-01 17:54:18 +0000181def ask_for_data(prompt):
182 try:
183 return raw_input(prompt)
184 except KeyboardInterrupt:
185 # Hide the exception.
186 sys.exit(1)
187
188
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100189def confirm_or_exit(prefix='', action='confirm'):
190 """Asks user to press enter to continue or press Ctrl+C to abort."""
191 if not prefix or prefix.endswith('\n'):
192 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100193 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100194 mid = ' Press'
195 elif prefix.endswith(' '):
196 mid = 'press'
197 else:
198 mid = ' press'
199 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
200
201
202def ask_for_explicit_yes(prompt):
203 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
204 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
205 while True:
206 if 'yes'.startswith(result):
207 return True
208 if 'no'.startswith(result):
209 return False
210 result = ask_for_data('Please, type yes or no: ').lower()
211
212
tandrii5d48c322016-08-18 16:19:37 -0700213def _git_branch_config_key(branch, key):
214 """Helper method to return Git config key for a branch."""
215 assert branch, 'branch name is required to set git config for it'
216 return 'branch.%s.%s' % (branch, key)
217
218
219def _git_get_branch_config_value(key, default=None, value_type=str,
220 branch=False):
221 """Returns git config value of given or current branch if any.
222
223 Returns default in all other cases.
224 """
225 assert value_type in (int, str, bool)
226 if branch is False: # Distinguishing default arg value from None.
227 branch = GetCurrentBranch()
228
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000229 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700230 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000231
tandrii5d48c322016-08-18 16:19:37 -0700232 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700233 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700234 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700235 # git config also has --int, but apparently git config suffers from integer
236 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700237 args.append(_git_branch_config_key(branch, key))
238 code, out = RunGitWithCode(args)
239 if code == 0:
240 value = out.strip()
241 if value_type == int:
242 return int(value)
243 if value_type == bool:
244 return bool(value.lower() == 'true')
245 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000246 return default
247
248
tandrii5d48c322016-08-18 16:19:37 -0700249def _git_set_branch_config_value(key, value, branch=None, **kwargs):
250 """Sets the value or unsets if it's None of a git branch config.
251
252 Valid, though not necessarily existing, branch must be provided,
253 otherwise currently checked out branch is used.
254 """
255 if not branch:
256 branch = GetCurrentBranch()
257 assert branch, 'a branch name OR currently checked out branch is required'
258 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700259 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700260 if value is None:
261 args.append('--unset')
262 elif isinstance(value, bool):
263 args.append('--bool')
264 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700265 else:
tandrii33a46ff2016-08-23 05:53:40 -0700266 # git config also has --int, but apparently git config suffers from integer
267 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700268 value = str(value)
269 args.append(_git_branch_config_key(branch, key))
270 if value is not None:
271 args.append(value)
272 RunGit(args, **kwargs)
273
274
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100275def _get_committer_timestamp(commit):
276 """Returns unix timestamp as integer of a committer in a commit.
277
278 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
279 """
280 # Git also stores timezone offset, but it only affects visual display,
281 # actual point in time is defined by this timestamp only.
282 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
283
284
285def _git_amend_head(message, committer_timestamp):
286 """Amends commit with new message and desired committer_timestamp.
287
288 Sets committer timezone to UTC.
289 """
290 env = os.environ.copy()
291 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
292 return RunGit(['commit', '--amend', '-m', message], env=env)
293
294
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000295def add_git_similarity(parser):
296 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700297 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000298 help='Sets the percentage that a pair of files need to match in order to'
299 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000300 parser.add_option(
301 '--find-copies', action='store_true',
302 help='Allows git to look for copies.')
303 parser.add_option(
304 '--no-find-copies', action='store_false', dest='find_copies',
305 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000306
307 old_parser_args = parser.parse_args
308 def Parse(args):
309 options, args = old_parser_args(args)
310
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000311 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700312 options.similarity = _git_get_branch_config_value(
313 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000314 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000315 print('Note: Saving similarity of %d%% in git config.'
316 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700317 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000318
iannucci@chromium.org79540052012-10-19 23:15:26 +0000319 options.similarity = max(0, min(options.similarity, 100))
320
321 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700322 options.find_copies = _git_get_branch_config_value(
323 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000324 else:
tandrii5d48c322016-08-18 16:19:37 -0700325 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000326
327 print('Using %d%% similarity for rename/copy detection. '
328 'Override with --similarity.' % options.similarity)
329
330 return options, args
331 parser.parse_args = Parse
332
333
machenbach@chromium.org45453142015-09-15 08:45:22 +0000334def _get_properties_from_options(options):
335 properties = dict(x.split('=', 1) for x in options.properties)
336 for key, val in properties.iteritems():
337 try:
338 properties[key] = json.loads(val)
339 except ValueError:
340 pass # If a value couldn't be evaluated, treat it as a string.
341 return properties
342
343
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000344def _prefix_master(master):
345 """Convert user-specified master name to full master name.
346
347 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
348 name, while the developers always use shortened master name
349 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
350 function does the conversion for buildbucket migration.
351 """
borenet6c0efe62016-10-19 08:13:29 -0700352 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000353 return master
borenet6c0efe62016-10-19 08:13:29 -0700354 return '%s%s' % (MASTER_PREFIX, master)
355
356
357def _unprefix_master(bucket):
358 """Convert bucket name to shortened master name.
359
360 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
361 name, while the developers always use shortened master name
362 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
363 function does the conversion for buildbucket migration.
364 """
365 if bucket.startswith(MASTER_PREFIX):
366 return bucket[len(MASTER_PREFIX):]
367 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368
369
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000370def _buildbucket_retry(operation_name, http, *args, **kwargs):
371 """Retries requests to buildbucket service and returns parsed json content."""
372 try_count = 0
373 while True:
374 response, content = http.request(*args, **kwargs)
375 try:
376 content_json = json.loads(content)
377 except ValueError:
378 content_json = None
379
380 # Buildbucket could return an error even if status==200.
381 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000382 error = content_json.get('error')
383 if error.get('code') == 403:
384 raise BuildbucketResponseException(
385 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000386 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000387 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000388 raise BuildbucketResponseException(msg)
389
390 if response.status == 200:
391 if not content_json:
392 raise BuildbucketResponseException(
393 'Buildbucket returns invalid json content: %s.\n'
394 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
395 content)
396 return content_json
397 if response.status < 500 or try_count >= 2:
398 raise httplib2.HttpLib2Error(content)
399
400 # status >= 500 means transient failures.
401 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700402 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000403 try_count += 1
404 assert False, 'unreachable'
405
406
qyearsley1fdfcb62016-10-24 13:22:03 -0700407def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700408 """Returns a dict mapping bucket names to builders and tests,
409 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 """
qyearsleydd49f942016-10-28 11:57:22 -0700411 # If no bots are listed, we try to get a set of builders and tests based
412 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700413 if not options.bot:
414 change = changelist.GetChange(
415 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700416 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700417 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700418 change=change,
419 changed_files=change.LocalPaths(),
420 repository_root=settings.GetRoot(),
421 default_presubmit=None,
422 project=None,
423 verbose=options.verbose,
424 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700425 if masters is None:
426 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100427 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700428
qyearsley1fdfcb62016-10-24 13:22:03 -0700429 if options.bucket:
430 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700431 if options.master:
432 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700433
qyearsleydd49f942016-10-28 11:57:22 -0700434 # If bots are listed but no master or bucket, then we need to find out
435 # the corresponding master for each bot.
436 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
437 if error_message:
438 option_parser.error(
439 'Tryserver master cannot be found because: %s\n'
440 'Please manually specify the tryserver master, e.g. '
441 '"-m tryserver.chromium.linux".' % error_message)
442 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700443
444
qyearsley123a4682016-10-26 09:12:17 -0700445def _get_bucket_map_for_builders(builders):
446 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700447 map_url = 'https://builders-map.appspot.com/'
448 try:
qyearsley123a4682016-10-26 09:12:17 -0700449 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700450 except urllib2.URLError as e:
451 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
452 (map_url, e))
453 except ValueError as e:
454 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700455 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700456 return None, 'Failed to build master map.'
457
qyearsley123a4682016-10-26 09:12:17 -0700458 bucket_map = {}
459 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700460 masters = builders_map.get(builder, [])
461 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700462 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700463 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700464 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700465 (builder, masters))
466 bucket = _prefix_master(masters[0])
467 bucket_map.setdefault(bucket, {})[builder] = []
468
469 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700470
471
borenet6c0efe62016-10-19 08:13:29 -0700472def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700473 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700474 """Sends a request to Buildbucket to trigger try jobs for a changelist.
475
476 Args:
477 auth_config: AuthConfig for Rietveld.
478 changelist: Changelist that the try jobs are associated with.
479 buckets: A nested dict mapping bucket names to builders to tests.
480 options: Command-line options.
481 """
tandriide281ae2016-10-12 06:02:30 -0700482 assert changelist.GetIssue(), 'CL must be uploaded first'
483 codereview_url = changelist.GetCodereviewServer()
484 assert codereview_url, 'CL must be uploaded first'
485 patchset = patchset or changelist.GetMostRecentPatchset()
486 assert patchset, 'CL must be uploaded first'
487
488 codereview_host = urlparse.urlparse(codereview_url).hostname
489 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000490 http = authenticator.authorize(httplib2.Http())
491 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700492
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000493 buildbucket_put_url = (
494 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000495 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700496 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
497 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
498 hostname=codereview_host,
499 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000500 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700501
502 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
503 shared_parameters_properties['category'] = category
504 if options.clobber:
505 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700506 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700507 if extra_properties:
508 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000509
510 batch_req_body = {'builds': []}
511 print_text = []
512 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700513 for bucket, builders_and_tests in sorted(buckets.iteritems()):
514 print_text.append('Bucket: %s' % bucket)
515 master = None
516 if bucket.startswith(MASTER_PREFIX):
517 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000518 for builder, tests in sorted(builders_and_tests.iteritems()):
519 print_text.append(' %s: %s' % (builder, tests))
520 parameters = {
521 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000522 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100523 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000524 'revision': options.revision,
525 }],
tandrii8c5a3532016-11-04 07:52:02 -0700526 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000527 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000528 if 'presubmit' in builder.lower():
529 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000530 if tests:
531 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700532
533 tags = [
534 'builder:%s' % builder,
535 'buildset:%s' % buildset,
536 'user_agent:git_cl_try',
537 ]
538 if master:
539 parameters['properties']['master'] = master
540 tags.append('master:%s' % master)
541
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000542 batch_req_body['builds'].append(
543 {
544 'bucket': bucket,
545 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700547 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000548 }
549 )
550
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700552 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 http,
554 buildbucket_put_url,
555 'PUT',
556 body=json.dumps(batch_req_body),
557 headers={'Content-Type': 'application/json'}
558 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000559 print_text.append('To see results here, run: git cl try-results')
560 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700561 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000562
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000563
tandrii221ab252016-10-06 08:12:04 -0700564def fetch_try_jobs(auth_config, changelist, buildbucket_host,
565 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700566 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567
qyearsley53f48a12016-09-01 10:45:13 -0700568 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 """
tandrii221ab252016-10-06 08:12:04 -0700570 assert buildbucket_host
571 assert changelist.GetIssue(), 'CL must be uploaded first'
572 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
573 patchset = patchset or changelist.GetMostRecentPatchset()
574 assert patchset, 'CL must be uploaded first'
575
576 codereview_url = changelist.GetCodereviewServer()
577 codereview_host = urlparse.urlparse(codereview_url).hostname
578 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 if authenticator.has_cached_credentials():
580 http = authenticator.authorize(httplib2.Http())
581 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700582 print('Warning: Some results might be missing because %s' %
583 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700584 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 http = httplib2.Http()
586
587 http.force_exception_to_status_code = True
588
tandrii221ab252016-10-06 08:12:04 -0700589 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
590 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
591 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000592 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700593 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000594 params = {'tag': 'buildset:%s' % buildset}
595
596 builds = {}
597 while True:
598 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700599 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000600 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700601 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000602 for build in content.get('builds', []):
603 builds[build['id']] = build
604 if 'next_cursor' in content:
605 params['start_cursor'] = content['next_cursor']
606 else:
607 break
608 return builds
609
610
qyearsleyeab3c042016-08-24 09:18:28 -0700611def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000612 """Prints nicely result of fetch_try_jobs."""
613 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700614 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000615 return
616
617 # Make a copy, because we'll be modifying builds dictionary.
618 builds = builds.copy()
619 builder_names_cache = {}
620
621 def get_builder(b):
622 try:
623 return builder_names_cache[b['id']]
624 except KeyError:
625 try:
626 parameters = json.loads(b['parameters_json'])
627 name = parameters['builder_name']
628 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700629 print('WARNING: failed to get builder name for build %s: %s' % (
630 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000631 name = None
632 builder_names_cache[b['id']] = name
633 return name
634
635 def get_bucket(b):
636 bucket = b['bucket']
637 if bucket.startswith('master.'):
638 return bucket[len('master.'):]
639 return bucket
640
641 if options.print_master:
642 name_fmt = '%%-%ds %%-%ds' % (
643 max(len(str(get_bucket(b))) for b in builds.itervalues()),
644 max(len(str(get_builder(b))) for b in builds.itervalues()))
645 def get_name(b):
646 return name_fmt % (get_bucket(b), get_builder(b))
647 else:
648 name_fmt = '%%-%ds' % (
649 max(len(str(get_builder(b))) for b in builds.itervalues()))
650 def get_name(b):
651 return name_fmt % get_builder(b)
652
653 def sort_key(b):
654 return b['status'], b.get('result'), get_name(b), b.get('url')
655
656 def pop(title, f, color=None, **kwargs):
657 """Pop matching builds from `builds` dict and print them."""
658
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000659 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000660 colorize = str
661 else:
662 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
663
664 result = []
665 for b in builds.values():
666 if all(b.get(k) == v for k, v in kwargs.iteritems()):
667 builds.pop(b['id'])
668 result.append(b)
669 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700670 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000671 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700672 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000673
674 total = len(builds)
675 pop(status='COMPLETED', result='SUCCESS',
676 title='Successes:', color=Fore.GREEN,
677 f=lambda b: (get_name(b), b.get('url')))
678 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
679 title='Infra Failures:', color=Fore.MAGENTA,
680 f=lambda b: (get_name(b), b.get('url')))
681 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
682 title='Failures:', color=Fore.RED,
683 f=lambda b: (get_name(b), b.get('url')))
684 pop(status='COMPLETED', result='CANCELED',
685 title='Canceled:', color=Fore.MAGENTA,
686 f=lambda b: (get_name(b),))
687 pop(status='COMPLETED', result='FAILURE',
688 failure_reason='INVALID_BUILD_DEFINITION',
689 title='Wrong master/builder name:', color=Fore.MAGENTA,
690 f=lambda b: (get_name(b),))
691 pop(status='COMPLETED', result='FAILURE',
692 title='Other failures:',
693 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
694 pop(status='COMPLETED',
695 title='Other finished:',
696 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
697 pop(status='STARTED',
698 title='Started:', color=Fore.YELLOW,
699 f=lambda b: (get_name(b), b.get('url')))
700 pop(status='SCHEDULED',
701 title='Scheduled:',
702 f=lambda b: (get_name(b), 'id=%s' % b['id']))
703 # The last section is just in case buildbucket API changes OR there is a bug.
704 pop(title='Other:',
705 f=lambda b: (get_name(b), 'id=%s' % b['id']))
706 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700707 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000708
709
qyearsley53f48a12016-09-01 10:45:13 -0700710def write_try_results_json(output_file, builds):
711 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
712
713 The input |builds| dict is assumed to be generated by Buildbucket.
714 Buildbucket documentation: http://goo.gl/G0s101
715 """
716
717 def convert_build_dict(build):
718 return {
719 'buildbucket_id': build.get('id'),
720 'status': build.get('status'),
721 'result': build.get('result'),
722 'bucket': build.get('bucket'),
723 'builder_name': json.loads(
724 build.get('parameters_json', '{}')).get('builder_name'),
725 'failure_reason': build.get('failure_reason'),
726 'url': build.get('url'),
727 }
728
729 converted = []
730 for _, build in sorted(builds.items()):
731 converted.append(convert_build_dict(build))
732 write_json(output_file, converted)
733
734
iannucci@chromium.org79540052012-10-19 23:15:26 +0000735def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000736 """Prints statistics about the change to the user."""
737 # --no-ext-diff is broken in some versions of Git, so try to work around
738 # this by overriding the environment (but there is still a problem if the
739 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000740 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000741 if 'GIT_EXTERNAL_DIFF' in env:
742 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000743
744 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800745 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000746 else:
747 similarity_options = ['-M%s' % similarity]
748
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000749 try:
750 stdout = sys.stdout.fileno()
751 except AttributeError:
752 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000753 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000754 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000755 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000756 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000757
758
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000759class BuildbucketResponseException(Exception):
760 pass
761
762
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000763class Settings(object):
764 def __init__(self):
765 self.default_server = None
766 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000767 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768 self.tree_status_url = None
769 self.viewvc_url = None
770 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000771 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000772 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000773 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000774 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000775 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000776 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777
778 def LazyUpdateIfNeeded(self):
779 """Updates the settings from a codereview.settings file, if available."""
780 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000781 # The only value that actually changes the behavior is
782 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000783 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000784 error_ok=True
785 ).strip().lower()
786
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000788 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789 LoadCodereviewSettingsFromFile(cr_settings_file)
790 self.updated = True
791
792 def GetDefaultServerUrl(self, error_ok=False):
793 if not self.default_server:
794 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000795 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000796 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 if error_ok:
798 return self.default_server
799 if not self.default_server:
800 error_message = ('Could not find settings file. You must configure '
801 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000802 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000803 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000804 return self.default_server
805
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000806 @staticmethod
807 def GetRelativeRoot():
808 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000811 if self.root is None:
812 self.root = os.path.abspath(self.GetRelativeRoot())
813 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000815 def GetGitMirror(self, remote='origin'):
816 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000817 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000818 if not os.path.isdir(local_url):
819 return None
820 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
821 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100822 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100823 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000824 if mirror.exists():
825 return mirror
826 return None
827
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828 def GetTreeStatusUrl(self, error_ok=False):
829 if not self.tree_status_url:
830 error_message = ('You must configure your tree status URL by running '
831 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000832 self.tree_status_url = self._GetRietveldConfig(
833 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 return self.tree_status_url
835
836 def GetViewVCUrl(self):
837 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000838 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839 return self.viewvc_url
840
rmistry@google.com90752582014-01-14 21:04:50 +0000841 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000842 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000843
rmistry@google.com78948ed2015-07-08 23:09:57 +0000844 def GetIsSkipDependencyUpload(self, branch_name):
845 """Returns true if specified branch should skip dep uploads."""
846 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
847 error_ok=True)
848
rmistry@google.com5626a922015-02-26 14:03:30 +0000849 def GetRunPostUploadHook(self):
850 run_post_upload_hook = self._GetRietveldConfig(
851 'run-post-upload-hook', error_ok=True)
852 return run_post_upload_hook == "True"
853
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000854 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000855 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000856
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000857 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000858 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000859
ukai@chromium.orge8077812012-02-03 03:41:46 +0000860 def GetIsGerrit(self):
861 """Return true if this repo is assosiated with gerrit code review system."""
862 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700863 self.is_gerrit = (
864 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000865 return self.is_gerrit
866
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000867 def GetSquashGerritUploads(self):
868 """Return true if uploads to Gerrit should be squashed by default."""
869 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700870 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
871 if self.squash_gerrit_uploads is None:
872 # Default is squash now (http://crbug.com/611892#c23).
873 self.squash_gerrit_uploads = not (
874 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
875 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000876 return self.squash_gerrit_uploads
877
tandriia60502f2016-06-20 02:01:53 -0700878 def GetSquashGerritUploadsOverride(self):
879 """Return True or False if codereview.settings should be overridden.
880
881 Returns None if no override has been defined.
882 """
883 # See also http://crbug.com/611892#c23
884 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
885 error_ok=True).strip()
886 if result == 'true':
887 return True
888 if result == 'false':
889 return False
890 return None
891
tandrii@chromium.org28253532016-04-14 13:46:56 +0000892 def GetGerritSkipEnsureAuthenticated(self):
893 """Return True if EnsureAuthenticated should not be done for Gerrit
894 uploads."""
895 if self.gerrit_skip_ensure_authenticated is None:
896 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000897 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000898 error_ok=True).strip() == 'true')
899 return self.gerrit_skip_ensure_authenticated
900
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000901 def GetGitEditor(self):
902 """Return the editor specified in the git config, or None if none is."""
903 if self.git_editor is None:
904 self.git_editor = self._GetConfig('core.editor', error_ok=True)
905 return self.git_editor or None
906
thestig@chromium.org44202a22014-03-11 19:22:18 +0000907 def GetLintRegex(self):
908 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
909 DEFAULT_LINT_REGEX)
910
911 def GetLintIgnoreRegex(self):
912 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
913 DEFAULT_LINT_IGNORE_REGEX)
914
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000915 def GetProject(self):
916 if not self.project:
917 self.project = self._GetRietveldConfig('project', error_ok=True)
918 return self.project
919
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000920 def _GetRietveldConfig(self, param, **kwargs):
921 return self._GetConfig('rietveld.' + param, **kwargs)
922
rmistry@google.com78948ed2015-07-08 23:09:57 +0000923 def _GetBranchConfig(self, branch_name, param, **kwargs):
924 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 def _GetConfig(self, param, **kwargs):
927 self.LazyUpdateIfNeeded()
928 return RunGit(['config', param], **kwargs).strip()
929
930
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100931@contextlib.contextmanager
932def _get_gerrit_project_config_file(remote_url):
933 """Context manager to fetch and store Gerrit's project.config from
934 refs/meta/config branch and store it in temp file.
935
936 Provides a temporary filename or None if there was error.
937 """
938 error, _ = RunGitWithCode([
939 'fetch', remote_url,
940 '+refs/meta/config:refs/git_cl/meta/config'])
941 if error:
942 # Ref doesn't exist or isn't accessible to current user.
943 print('WARNING: failed to fetch project config for %s: %s' %
944 (remote_url, error))
945 yield None
946 return
947
948 error, project_config_data = RunGitWithCode(
949 ['show', 'refs/git_cl/meta/config:project.config'])
950 if error:
951 print('WARNING: project.config file not found')
952 yield None
953 return
954
955 with gclient_utils.temporary_directory() as tempdir:
956 project_config_file = os.path.join(tempdir, 'project.config')
957 gclient_utils.FileWrite(project_config_file, project_config_data)
958 yield project_config_file
959
960
961def _is_git_numberer_enabled(remote_url, remote_ref):
962 """Returns True if Git Numberer is enabled on this ref."""
963 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100964 KNOWN_PROJECTS_WHITELIST = [
965 'chromium/src',
966 'external/webrtc',
967 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100968 'infra/experimental',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100969 ]
970
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100971 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
972 url_parts = urlparse.urlparse(remote_url)
973 project_name = url_parts.path.lstrip('/').rstrip('git./')
974 for known in KNOWN_PROJECTS_WHITELIST:
975 if project_name.endswith(known):
976 break
977 else:
978 # Early exit to avoid extra fetches for repos that aren't using Git
979 # Numberer.
980 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100981
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100982 with _get_gerrit_project_config_file(remote_url) as project_config_file:
983 if project_config_file is None:
984 # Failed to fetch project.config, which shouldn't happen on open source
985 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100986 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100987 def get_opts(x):
988 code, out = RunGitWithCode(
989 ['config', '-f', project_config_file, '--get-all',
990 'plugin.git-numberer.validate-%s-refglob' % x])
991 if code == 0:
992 return out.strip().splitlines()
993 return []
994 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100995
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100996 logging.info('validator config enabled %s disabled %s refglobs for '
997 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000998
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100999 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001000 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001001 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001002 return True
1003 return False
1004
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001005 if match_refglobs(disabled):
1006 return False
1007 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001008
1009
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010def ShortBranchName(branch):
1011 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001012 return branch.replace('refs/heads/', '', 1)
1013
1014
1015def GetCurrentBranchRef():
1016 """Returns branch ref (e.g., refs/heads/master) or None."""
1017 return RunGit(['symbolic-ref', 'HEAD'],
1018 stderr=subprocess2.VOID, error_ok=True).strip() or None
1019
1020
1021def GetCurrentBranch():
1022 """Returns current branch or None.
1023
1024 For refs/heads/* branches, returns just last part. For others, full ref.
1025 """
1026 branchref = GetCurrentBranchRef()
1027 if branchref:
1028 return ShortBranchName(branchref)
1029 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030
1031
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001032class _CQState(object):
1033 """Enum for states of CL with respect to Commit Queue."""
1034 NONE = 'none'
1035 DRY_RUN = 'dry_run'
1036 COMMIT = 'commit'
1037
1038 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1039
1040
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001041class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001042 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001043 self.issue = issue
1044 self.patchset = patchset
1045 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001046 assert codereview in (None, 'rietveld', 'gerrit')
1047 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001048
1049 @property
1050 def valid(self):
1051 return self.issue is not None
1052
1053
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001054def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001055 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1056 fail_result = _ParsedIssueNumberArgument()
1057
1058 if arg.isdigit():
1059 return _ParsedIssueNumberArgument(issue=int(arg))
1060 if not arg.startswith('http'):
1061 return fail_result
1062 url = gclient_utils.UpgradeToHttps(arg)
1063 try:
1064 parsed_url = urlparse.urlparse(url)
1065 except ValueError:
1066 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001067
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001068 if codereview is not None:
1069 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1070 return parsed or fail_result
1071
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001072 results = {}
1073 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1074 parsed = cls.ParseIssueURL(parsed_url)
1075 if parsed is not None:
1076 results[name] = parsed
1077
1078 if not results:
1079 return fail_result
1080 if len(results) == 1:
1081 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001082
1083 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1084 # This is likely Gerrit.
1085 return results['gerrit']
1086 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001087 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001088
1089
Aaron Gablea45ee112016-11-22 15:14:38 -08001090class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001091 def __init__(self, issue, url):
1092 self.issue = issue
1093 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001094 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001095
1096 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001097 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001098 self.issue, self.url)
1099
1100
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001101_CommentSummary = collections.namedtuple(
1102 '_CommentSummary', ['date', 'message', 'sender',
1103 # TODO(tandrii): these two aren't known in Gerrit.
1104 'approval', 'disapproval'])
1105
1106
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001107class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001108 """Changelist works with one changelist in local branch.
1109
1110 Supports two codereview backends: Rietveld or Gerrit, selected at object
1111 creation.
1112
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001113 Notes:
1114 * Not safe for concurrent multi-{thread,process} use.
1115 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001116 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001117 """
1118
1119 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1120 """Create a new ChangeList instance.
1121
1122 If issue is given, the codereview must be given too.
1123
1124 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1125 Otherwise, it's decided based on current configuration of the local branch,
1126 with default being 'rietveld' for backwards compatibility.
1127 See _load_codereview_impl for more details.
1128
1129 **kwargs will be passed directly to codereview implementation.
1130 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001132 global settings
1133 if not settings:
1134 # Happens when git_cl.py is used as a utility library.
1135 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001136
1137 if issue:
1138 assert codereview, 'codereview must be known, if issue is known'
1139
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 self.branchref = branchref
1141 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001142 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 self.branch = ShortBranchName(self.branchref)
1144 else:
1145 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001147 self.lookedup_issue = False
1148 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 self.has_description = False
1150 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001151 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153 self.cc = None
1154 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001155 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001156
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001157 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001158 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001159 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001160 assert self._codereview_impl
1161 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001162
1163 def _load_codereview_impl(self, codereview=None, **kwargs):
1164 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001165 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1166 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1167 self._codereview = codereview
1168 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169 return
1170
1171 # Automatic selection based on issue number set for a current branch.
1172 # Rietveld takes precedence over Gerrit.
1173 assert not self.issue
1174 # Whether we find issue or not, we are doing the lookup.
1175 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001176 if self.GetBranch():
1177 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1178 issue = _git_get_branch_config_value(
1179 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1180 if issue:
1181 self._codereview = codereview
1182 self._codereview_impl = cls(self, **kwargs)
1183 self.issue = int(issue)
1184 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001185
1186 # No issue is set for this branch, so decide based on repo-wide settings.
1187 return self._load_codereview_impl(
1188 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1189 **kwargs)
1190
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001191 def IsGerrit(self):
1192 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001193
1194 def GetCCList(self):
1195 """Return the users cc'd on this CL.
1196
agable92bec4f2016-08-24 09:27:27 -07001197 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001198 """
1199 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001200 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001201 more_cc = ','.join(self.watchers)
1202 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1203 return self.cc
1204
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001205 def GetCCListWithoutDefault(self):
1206 """Return the users cc'd on this CL excluding default ones."""
1207 if self.cc is None:
1208 self.cc = ','.join(self.watchers)
1209 return self.cc
1210
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001211 def SetWatchers(self, watchers):
1212 """Set the list of email addresses that should be cc'd based on the changed
1213 files in this CL.
1214 """
1215 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216
1217 def GetBranch(self):
1218 """Returns the short branch name, e.g. 'master'."""
1219 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001220 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001221 if not branchref:
1222 return None
1223 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 self.branch = ShortBranchName(self.branchref)
1225 return self.branch
1226
1227 def GetBranchRef(self):
1228 """Returns the full branch name, e.g. 'refs/heads/master'."""
1229 self.GetBranch() # Poke the lazy loader.
1230 return self.branchref
1231
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001232 def ClearBranch(self):
1233 """Clears cached branch data of this object."""
1234 self.branch = self.branchref = None
1235
tandrii5d48c322016-08-18 16:19:37 -07001236 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1237 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1238 kwargs['branch'] = self.GetBranch()
1239 return _git_get_branch_config_value(key, default, **kwargs)
1240
1241 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1242 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1243 assert self.GetBranch(), (
1244 'this CL must have an associated branch to %sset %s%s' %
1245 ('un' if value is None else '',
1246 key,
1247 '' if value is None else ' to %r' % value))
1248 kwargs['branch'] = self.GetBranch()
1249 return _git_set_branch_config_value(key, value, **kwargs)
1250
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001251 @staticmethod
1252 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001253 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 e.g. 'origin', 'refs/heads/master'
1255 """
1256 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001257 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1258
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001260 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001262 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1263 error_ok=True).strip()
1264 if upstream_branch:
1265 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001267 # Else, try to guess the origin remote.
1268 remote_branches = RunGit(['branch', '-r']).split()
1269 if 'origin/master' in remote_branches:
1270 # Fall back on origin/master if it exits.
1271 remote = 'origin'
1272 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001274 DieWithError(
1275 'Unable to determine default branch to diff against.\n'
1276 'Either pass complete "git diff"-style arguments, like\n'
1277 ' git cl upload origin/master\n'
1278 'or verify this branch is set up to track another \n'
1279 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280
1281 return remote, upstream_branch
1282
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001283 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001284 upstream_branch = self.GetUpstreamBranch()
1285 if not BranchExists(upstream_branch):
1286 DieWithError('The upstream for the current branch (%s) does not exist '
1287 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001288 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001289 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 def GetUpstreamBranch(self):
1292 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001293 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001295 upstream_branch = upstream_branch.replace('refs/heads/',
1296 'refs/remotes/%s/' % remote)
1297 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1298 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299 self.upstream_branch = upstream_branch
1300 return self.upstream_branch
1301
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001302 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001303 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001304 remote, branch = None, self.GetBranch()
1305 seen_branches = set()
1306 while branch not in seen_branches:
1307 seen_branches.add(branch)
1308 remote, branch = self.FetchUpstreamTuple(branch)
1309 branch = ShortBranchName(branch)
1310 if remote != '.' or branch.startswith('refs/remotes'):
1311 break
1312 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001313 remotes = RunGit(['remote'], error_ok=True).split()
1314 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001315 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001316 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001317 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001318 logging.warn('Could not determine which remote this change is '
1319 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001320 else:
1321 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001322 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001323 branch = 'HEAD'
1324 if branch.startswith('refs/remotes'):
1325 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001326 elif branch.startswith('refs/branch-heads/'):
1327 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001328 else:
1329 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001330 return self._remote
1331
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001332 def GitSanityChecks(self, upstream_git_obj):
1333 """Checks git repo status and ensures diff is from local commits."""
1334
sbc@chromium.org79706062015-01-14 21:18:12 +00001335 if upstream_git_obj is None:
1336 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001337 print('ERROR: unable to determine current branch (detached HEAD?)',
1338 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001339 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001340 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001341 return False
1342
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001343 # Verify the commit we're diffing against is in our current branch.
1344 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1345 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1346 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001347 print('ERROR: %s is not in the current branch. You may need to rebase '
1348 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001349 return False
1350
1351 # List the commits inside the diff, and verify they are all local.
1352 commits_in_diff = RunGit(
1353 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1354 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1355 remote_branch = remote_branch.strip()
1356 if code != 0:
1357 _, remote_branch = self.GetRemoteBranch()
1358
1359 commits_in_remote = RunGit(
1360 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1361
1362 common_commits = set(commits_in_diff) & set(commits_in_remote)
1363 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001364 print('ERROR: Your diff contains %d commits already in %s.\n'
1365 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1366 'the diff. If you are using a custom git flow, you can override'
1367 ' the reference used for this check with "git config '
1368 'gitcl.remotebranch <git-ref>".' % (
1369 len(common_commits), remote_branch, upstream_git_obj),
1370 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001371 return False
1372 return True
1373
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001374 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001375 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001376
1377 Returns None if it is not set.
1378 """
tandrii5d48c322016-08-18 16:19:37 -07001379 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001380
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381 def GetRemoteUrl(self):
1382 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1383
1384 Returns None if there is no remote.
1385 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001386 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001387 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1388
1389 # If URL is pointing to a local directory, it is probably a git cache.
1390 if os.path.isdir(url):
1391 url = RunGit(['config', 'remote.%s.url' % remote],
1392 error_ok=True,
1393 cwd=url).strip()
1394 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001396 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001397 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001398 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001399 self.issue = self._GitGetBranchConfigValue(
1400 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001401 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 return self.issue
1403
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 def GetIssueURL(self):
1405 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001406 issue = self.GetIssue()
1407 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001408 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001409 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001411 def GetDescription(self, pretty=False, force=False):
1412 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001414 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 self.has_description = True
1416 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001417 # Set width to 72 columns + 2 space indent.
1418 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001420 lines = self.description.splitlines()
1421 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 return self.description
1423
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001424 def GetDescriptionFooters(self):
1425 """Returns (non_footer_lines, footers) for the commit message.
1426
1427 Returns:
1428 non_footer_lines (list(str)) - Simple list of description lines without
1429 any footer. The lines do not contain newlines, nor does the list contain
1430 the empty line between the message and the footers.
1431 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1432 [("Change-Id", "Ideadbeef...."), ...]
1433 """
1434 raw_description = self.GetDescription()
1435 msg_lines, _, footers = git_footers.split_footers(raw_description)
1436 if footers:
1437 msg_lines = msg_lines[:len(msg_lines)-1]
1438 return msg_lines, footers
1439
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001441 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001442 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001443 self.patchset = self._GitGetBranchConfigValue(
1444 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001445 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 return self.patchset
1447
1448 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001449 """Set this branch's patchset. If patchset=0, clears the patchset."""
1450 assert self.GetBranch()
1451 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001452 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001453 else:
1454 self.patchset = int(patchset)
1455 self._GitSetBranchConfigValue(
1456 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001458 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001459 """Set this branch's issue. If issue isn't given, clears the issue."""
1460 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001462 issue = int(issue)
1463 self._GitSetBranchConfigValue(
1464 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001465 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001466 codereview_server = self._codereview_impl.GetCodereviewServer()
1467 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001468 self._GitSetBranchConfigValue(
1469 self._codereview_impl.CodereviewServerConfigKey(),
1470 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001471 else:
tandrii5d48c322016-08-18 16:19:37 -07001472 # Reset all of these just to be clean.
1473 reset_suffixes = [
1474 'last-upload-hash',
1475 self._codereview_impl.IssueConfigKey(),
1476 self._codereview_impl.PatchsetConfigKey(),
1477 self._codereview_impl.CodereviewServerConfigKey(),
1478 ] + self._PostUnsetIssueProperties()
1479 for prop in reset_suffixes:
1480 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001481 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001482 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001483
dnjba1b0f32016-09-02 12:37:42 -07001484 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001485 if not self.GitSanityChecks(upstream_branch):
1486 DieWithError('\nGit sanity check failure')
1487
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001488 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001489 if not root:
1490 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001491 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001492
1493 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001494 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001495 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001496 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001497 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001498 except subprocess2.CalledProcessError:
1499 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001500 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001501 'This branch probably doesn\'t exist anymore. To reset the\n'
1502 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001503 ' git branch --set-upstream-to origin/master %s\n'
1504 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001505 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001506
maruel@chromium.org52424302012-08-29 15:14:30 +00001507 issue = self.GetIssue()
1508 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001509 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001510 description = self.GetDescription()
1511 else:
1512 # If the change was never uploaded, use the log messages of all commits
1513 # up to the branch point, as git cl upload will prefill the description
1514 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001515 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1516 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001517
1518 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001519 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001520 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001521 name,
1522 description,
1523 absroot,
1524 files,
1525 issue,
1526 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001527 author,
1528 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001529
dsansomee2d6fd92016-09-08 00:10:47 -07001530 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001531 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001532 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001533 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001534
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001535 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1536 """Sets the description for this CL remotely.
1537
1538 You can get description_lines and footers with GetDescriptionFooters.
1539
1540 Args:
1541 description_lines (list(str)) - List of CL description lines without
1542 newline characters.
1543 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1544 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1545 `List-Of-Tokens`). It will be case-normalized so that each token is
1546 title-cased.
1547 """
1548 new_description = '\n'.join(description_lines)
1549 if footers:
1550 new_description += '\n'
1551 for k, v in footers:
1552 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1553 if not git_footers.FOOTER_PATTERN.match(foot):
1554 raise ValueError('Invalid footer %r' % foot)
1555 new_description += foot + '\n'
1556 self.UpdateDescription(new_description, force)
1557
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001558 def RunHook(self, committing, may_prompt, verbose, change):
1559 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1560 try:
1561 return presubmit_support.DoPresubmitChecks(change, committing,
1562 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1563 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001564 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1565 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001566 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001567 DieWithError(
1568 ('%s\nMaybe your depot_tools is out of date?\n'
1569 'If all fails, contact maruel@') % e)
1570
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001571 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1572 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001573 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1574 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001575 else:
1576 # Assume url.
1577 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1578 urlparse.urlparse(issue_arg))
1579 if not parsed_issue_arg or not parsed_issue_arg.valid:
1580 DieWithError('Failed to parse issue argument "%s". '
1581 'Must be an issue number or a valid URL.' % issue_arg)
1582 return self._codereview_impl.CMDPatchWithParsedIssue(
1583 parsed_issue_arg, reject, nocommit, directory)
1584
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001585 def CMDUpload(self, options, git_diff_args, orig_args):
1586 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001587 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001588 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001589 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001590 else:
1591 if self.GetBranch() is None:
1592 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1593
1594 # Default to diffing against common ancestor of upstream branch
1595 base_branch = self.GetCommonAncestorWithUpstream()
1596 git_diff_args = [base_branch, 'HEAD']
1597
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001598 # Fast best-effort checks to abort before running potentially
1599 # expensive hooks if uploading is likely to fail anyway. Passing these
1600 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001601 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001602 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001603
1604 # Apply watchlists on upload.
1605 change = self.GetChange(base_branch, None)
1606 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1607 files = [f.LocalPath() for f in change.AffectedFiles()]
1608 if not options.bypass_watchlists:
1609 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1610
1611 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001612 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001613 # Set the reviewer list now so that presubmit checks can access it.
1614 change_description = ChangeDescription(change.FullDescriptionText())
1615 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001616 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001617 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001618 change)
1619 change.SetDescriptionText(change_description.description)
1620 hook_results = self.RunHook(committing=False,
1621 may_prompt=not options.force,
1622 verbose=options.verbose,
1623 change=change)
1624 if not hook_results.should_continue():
1625 return 1
1626 if not options.reviewers and hook_results.reviewers:
1627 options.reviewers = hook_results.reviewers.split(',')
1628
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001629 # TODO(tandrii): Checking local patchset against remote patchset is only
1630 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1631 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001632 latest_patchset = self.GetMostRecentPatchset()
1633 local_patchset = self.GetPatchset()
1634 if (latest_patchset and local_patchset and
1635 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001636 print('The last upload made from this repository was patchset #%d but '
1637 'the most recent patchset on the server is #%d.'
1638 % (local_patchset, latest_patchset))
1639 print('Uploading will still work, but if you\'ve uploaded to this '
1640 'issue from another machine or branch the patch you\'re '
1641 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001642 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001643
1644 print_stats(options.similarity, options.find_copies, git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001645 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001646 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001647 if options.use_commit_queue:
1648 self.SetCQState(_CQState.COMMIT)
1649 elif options.cq_dry_run:
1650 self.SetCQState(_CQState.DRY_RUN)
1651
tandrii5d48c322016-08-18 16:19:37 -07001652 _git_set_branch_config_value('last-upload-hash',
1653 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001654 # Run post upload hooks, if specified.
1655 if settings.GetRunPostUploadHook():
1656 presubmit_support.DoPostUploadExecuter(
1657 change,
1658 self,
1659 settings.GetRoot(),
1660 options.verbose,
1661 sys.stdout)
1662
1663 # Upload all dependencies if specified.
1664 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001665 print()
1666 print('--dependencies has been specified.')
1667 print('All dependent local branches will be re-uploaded.')
1668 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001669 # Remove the dependencies flag from args so that we do not end up in a
1670 # loop.
1671 orig_args.remove('--dependencies')
1672 ret = upload_branch_deps(self, orig_args)
1673 return ret
1674
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001675 def SetCQState(self, new_state):
1676 """Update the CQ state for latest patchset.
1677
1678 Issue must have been already uploaded and known.
1679 """
1680 assert new_state in _CQState.ALL_STATES
1681 assert self.GetIssue()
1682 return self._codereview_impl.SetCQState(new_state)
1683
qyearsley1fdfcb62016-10-24 13:22:03 -07001684 def TriggerDryRun(self):
1685 """Triggers a dry run and prints a warning on failure."""
1686 # TODO(qyearsley): Either re-use this method in CMDset_commit
1687 # and CMDupload, or change CMDtry to trigger dry runs with
1688 # just SetCQState, and catch keyboard interrupt and other
1689 # errors in that method.
1690 try:
1691 self.SetCQState(_CQState.DRY_RUN)
1692 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1693 return 0
1694 except KeyboardInterrupt:
1695 raise
1696 except:
1697 print('WARNING: failed to trigger CQ Dry Run.\n'
1698 'Either:\n'
1699 ' * your project has no CQ\n'
1700 ' * you don\'t have permission to trigger Dry Run\n'
1701 ' * bug in this code (see stack trace below).\n'
1702 'Consider specifying which bots to trigger manually '
1703 'or asking your project owners for permissions '
1704 'or contacting Chrome Infrastructure team at '
1705 'https://www.chromium.org/infra\n\n')
1706 # Still raise exception so that stack trace is printed.
1707 raise
1708
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001709 # Forward methods to codereview specific implementation.
1710
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001711 def AddComment(self, message):
1712 return self._codereview_impl.AddComment(message)
1713
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001714 def GetCommentsSummary(self):
1715 """Returns list of _CommentSummary for each comment.
1716
1717 Note: comments per file or per line are not included,
1718 only top-level comments are returned.
1719 """
1720 return self._codereview_impl.GetCommentsSummary()
1721
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001722 def CloseIssue(self):
1723 return self._codereview_impl.CloseIssue()
1724
1725 def GetStatus(self):
1726 return self._codereview_impl.GetStatus()
1727
1728 def GetCodereviewServer(self):
1729 return self._codereview_impl.GetCodereviewServer()
1730
tandriide281ae2016-10-12 06:02:30 -07001731 def GetIssueOwner(self):
1732 """Get owner from codereview, which may differ from this checkout."""
1733 return self._codereview_impl.GetIssueOwner()
1734
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001735 def GetMostRecentPatchset(self):
1736 return self._codereview_impl.GetMostRecentPatchset()
1737
tandriide281ae2016-10-12 06:02:30 -07001738 def CannotTriggerTryJobReason(self):
1739 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1740 return self._codereview_impl.CannotTriggerTryJobReason()
1741
tandrii8c5a3532016-11-04 07:52:02 -07001742 def GetTryjobProperties(self, patchset=None):
1743 """Returns dictionary of properties to launch tryjob."""
1744 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1745
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001746 def __getattr__(self, attr):
1747 # This is because lots of untested code accesses Rietveld-specific stuff
1748 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001749 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001750 # Note that child method defines __getattr__ as well, and forwards it here,
1751 # because _RietveldChangelistImpl is not cleaned up yet, and given
1752 # deprecation of Rietveld, it should probably be just removed.
1753 # Until that time, avoid infinite recursion by bypassing __getattr__
1754 # of implementation class.
1755 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001756
1757
1758class _ChangelistCodereviewBase(object):
1759 """Abstract base class encapsulating codereview specifics of a changelist."""
1760 def __init__(self, changelist):
1761 self._changelist = changelist # instance of Changelist
1762
1763 def __getattr__(self, attr):
1764 # Forward methods to changelist.
1765 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1766 # _RietveldChangelistImpl to avoid this hack?
1767 return getattr(self._changelist, attr)
1768
1769 def GetStatus(self):
1770 """Apply a rough heuristic to give a simple summary of an issue's review
1771 or CQ status, assuming adherence to a common workflow.
1772
1773 Returns None if no issue for this branch, or specific string keywords.
1774 """
1775 raise NotImplementedError()
1776
1777 def GetCodereviewServer(self):
1778 """Returns server URL without end slash, like "https://codereview.com"."""
1779 raise NotImplementedError()
1780
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001781 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001782 """Fetches and returns description from the codereview server."""
1783 raise NotImplementedError()
1784
tandrii5d48c322016-08-18 16:19:37 -07001785 @classmethod
1786 def IssueConfigKey(cls):
1787 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001788 raise NotImplementedError()
1789
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001790 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001791 def PatchsetConfigKey(cls):
1792 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001793 raise NotImplementedError()
1794
tandrii5d48c322016-08-18 16:19:37 -07001795 @classmethod
1796 def CodereviewServerConfigKey(cls):
1797 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001798 raise NotImplementedError()
1799
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001800 def _PostUnsetIssueProperties(self):
1801 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001802 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001803
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001804 def GetRieveldObjForPresubmit(self):
1805 # This is an unfortunate Rietveld-embeddedness in presubmit.
1806 # For non-Rietveld codereviews, this probably should return a dummy object.
1807 raise NotImplementedError()
1808
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001809 def GetGerritObjForPresubmit(self):
1810 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1811 return None
1812
dsansomee2d6fd92016-09-08 00:10:47 -07001813 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001814 """Update the description on codereview site."""
1815 raise NotImplementedError()
1816
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001817 def AddComment(self, message):
1818 """Posts a comment to the codereview site."""
1819 raise NotImplementedError()
1820
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001821 def GetCommentsSummary(self):
1822 raise NotImplementedError()
1823
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001824 def CloseIssue(self):
1825 """Closes the issue."""
1826 raise NotImplementedError()
1827
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001828 def GetMostRecentPatchset(self):
1829 """Returns the most recent patchset number from the codereview site."""
1830 raise NotImplementedError()
1831
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001832 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1833 directory):
1834 """Fetches and applies the issue.
1835
1836 Arguments:
1837 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1838 reject: if True, reject the failed patch instead of switching to 3-way
1839 merge. Rietveld only.
1840 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1841 only.
1842 directory: switch to directory before applying the patch. Rietveld only.
1843 """
1844 raise NotImplementedError()
1845
1846 @staticmethod
1847 def ParseIssueURL(parsed_url):
1848 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1849 failed."""
1850 raise NotImplementedError()
1851
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001852 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001853 """Best effort check that user is authenticated with codereview server.
1854
1855 Arguments:
1856 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001857 refresh: whether to attempt to refresh credentials. Ignored if not
1858 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001859 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001860 raise NotImplementedError()
1861
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001862 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001863 """Best effort check that uploading isn't supposed to fail for predictable
1864 reasons.
1865
1866 This method should raise informative exception if uploading shouldn't
1867 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001868
1869 Arguments:
1870 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001871 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001872 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001873
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001874 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001875 """Uploads a change to codereview."""
1876 raise NotImplementedError()
1877
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001878 def SetCQState(self, new_state):
1879 """Update the CQ state for latest patchset.
1880
1881 Issue must have been already uploaded and known.
1882 """
1883 raise NotImplementedError()
1884
tandriie113dfd2016-10-11 10:20:12 -07001885 def CannotTriggerTryJobReason(self):
1886 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1887 raise NotImplementedError()
1888
tandriide281ae2016-10-12 06:02:30 -07001889 def GetIssueOwner(self):
1890 raise NotImplementedError()
1891
tandrii8c5a3532016-11-04 07:52:02 -07001892 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001893 raise NotImplementedError()
1894
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001895
1896class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001897 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001898 super(_RietveldChangelistImpl, self).__init__(changelist)
1899 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001900 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001901 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001902
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001903 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001904 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001905 self._props = None
1906 self._rpc_server = None
1907
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001908 def GetCodereviewServer(self):
1909 if not self._rietveld_server:
1910 # If we're on a branch then get the server potentially associated
1911 # with that branch.
1912 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001913 self._rietveld_server = gclient_utils.UpgradeToHttps(
1914 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001915 if not self._rietveld_server:
1916 self._rietveld_server = settings.GetDefaultServerUrl()
1917 return self._rietveld_server
1918
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001919 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001920 """Best effort check that user is authenticated with Rietveld server."""
1921 if self._auth_config.use_oauth2:
1922 authenticator = auth.get_authenticator_for_host(
1923 self.GetCodereviewServer(), self._auth_config)
1924 if not authenticator.has_cached_credentials():
1925 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001926 if refresh:
1927 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001928
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001929 def EnsureCanUploadPatchset(self, force):
1930 # No checks for Rietveld because we are deprecating Rietveld.
1931 pass
1932
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001933 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001934 issue = self.GetIssue()
1935 assert issue
1936 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001937 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001938 except urllib2.HTTPError as e:
1939 if e.code == 404:
1940 DieWithError(
1941 ('\nWhile fetching the description for issue %d, received a '
1942 '404 (not found)\n'
1943 'error. It is likely that you deleted this '
1944 'issue on the server. If this is the\n'
1945 'case, please run\n\n'
1946 ' git cl issue 0\n\n'
1947 'to clear the association with the deleted issue. Then run '
1948 'this command again.') % issue)
1949 else:
1950 DieWithError(
1951 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1952 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001953 print('Warning: Failed to retrieve CL description due to network '
1954 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001955 return ''
1956
1957 def GetMostRecentPatchset(self):
1958 return self.GetIssueProperties()['patchsets'][-1]
1959
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001960 def GetIssueProperties(self):
1961 if self._props is None:
1962 issue = self.GetIssue()
1963 if not issue:
1964 self._props = {}
1965 else:
1966 self._props = self.RpcServer().get_issue_properties(issue, True)
1967 return self._props
1968
tandriie113dfd2016-10-11 10:20:12 -07001969 def CannotTriggerTryJobReason(self):
1970 props = self.GetIssueProperties()
1971 if not props:
1972 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1973 if props.get('closed'):
1974 return 'CL %s is closed' % self.GetIssue()
1975 if props.get('private'):
1976 return 'CL %s is private' % self.GetIssue()
1977 return None
1978
tandrii8c5a3532016-11-04 07:52:02 -07001979 def GetTryjobProperties(self, patchset=None):
1980 """Returns dictionary of properties to launch tryjob."""
1981 project = (self.GetIssueProperties() or {}).get('project')
1982 return {
1983 'issue': self.GetIssue(),
1984 'patch_project': project,
1985 'patch_storage': 'rietveld',
1986 'patchset': patchset or self.GetPatchset(),
1987 'rietveld': self.GetCodereviewServer(),
1988 }
1989
tandriide281ae2016-10-12 06:02:30 -07001990 def GetIssueOwner(self):
1991 return (self.GetIssueProperties() or {}).get('owner_email')
1992
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001993 def AddComment(self, message):
1994 return self.RpcServer().add_comment(self.GetIssue(), message)
1995
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001996 def GetCommentsSummary(self):
1997 summary = []
1998 for message in self.GetIssueProperties().get('messages', []):
1999 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2000 summary.append(_CommentSummary(
2001 date=date,
2002 disapproval=bool(message['disapproval']),
2003 approval=bool(message['approval']),
2004 sender=message['sender'],
2005 message=message['text'],
2006 ))
2007 return summary
2008
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002009 def GetStatus(self):
2010 """Apply a rough heuristic to give a simple summary of an issue's review
2011 or CQ status, assuming adherence to a common workflow.
2012
2013 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002014 * 'error' - error from review tool (including deleted issues)
2015 * 'unsent' - not sent for review
2016 * 'waiting' - waiting for review
2017 * 'reply' - waiting for owner to reply to review
2018 * 'not lgtm' - Code-Review label has been set negatively
2019 * 'lgtm' - LGTM from at least one approved reviewer
2020 * 'commit' - in the commit queue
2021 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002022 """
2023 if not self.GetIssue():
2024 return None
2025
2026 try:
2027 props = self.GetIssueProperties()
2028 except urllib2.HTTPError:
2029 return 'error'
2030
2031 if props.get('closed'):
2032 # Issue is closed.
2033 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002034 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002035 # Issue is in the commit queue.
2036 return 'commit'
2037
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002038 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002039 if not messages:
2040 # No message was sent.
2041 return 'unsent'
2042
2043 if get_approving_reviewers(props):
2044 return 'lgtm'
2045 elif get_approving_reviewers(props, disapproval=True):
2046 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002047
tandrii9d2c7a32016-06-22 03:42:45 -07002048 # Skip CQ messages that don't require owner's action.
2049 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2050 if 'Dry run:' in messages[-1]['text']:
2051 messages.pop()
2052 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2053 # This message always follows prior messages from CQ,
2054 # so skip this too.
2055 messages.pop()
2056 else:
2057 # This is probably a CQ messages warranting user attention.
2058 break
2059
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002060 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002061 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002062 return 'reply'
2063 return 'waiting'
2064
dsansomee2d6fd92016-09-08 00:10:47 -07002065 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002066 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002067
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002069 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002071 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002072 return self.SetFlags({flag: value})
2073
2074 def SetFlags(self, flags):
2075 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002076 """
phajdan.jr68598232016-08-10 03:28:28 -07002077 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002078 try:
tandrii4b233bd2016-07-06 03:50:29 -07002079 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002080 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002081 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002082 if e.code == 404:
2083 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2084 if e.code == 403:
2085 DieWithError(
2086 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002087 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002088 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002089
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002090 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002091 """Returns an upload.RpcServer() to access this review's rietveld instance.
2092 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002093 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002094 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002095 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002096 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002097 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002099 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002100 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002101 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002102
tandrii5d48c322016-08-18 16:19:37 -07002103 @classmethod
2104 def PatchsetConfigKey(cls):
2105 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106
tandrii5d48c322016-08-18 16:19:37 -07002107 @classmethod
2108 def CodereviewServerConfigKey(cls):
2109 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002110
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002111 def GetRieveldObjForPresubmit(self):
2112 return self.RpcServer()
2113
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002114 def SetCQState(self, new_state):
2115 props = self.GetIssueProperties()
2116 if props.get('private'):
2117 DieWithError('Cannot set-commit on private issue')
2118
2119 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002120 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002121 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002122 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002123 else:
tandrii4b233bd2016-07-06 03:50:29 -07002124 assert new_state == _CQState.DRY_RUN
2125 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002126
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002127 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2128 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002129 # PatchIssue should never be called with a dirty tree. It is up to the
2130 # caller to check this, but just in case we assert here since the
2131 # consequences of the caller not checking this could be dire.
2132 assert(not git_common.is_dirty_git_tree('apply'))
2133 assert(parsed_issue_arg.valid)
2134 self._changelist.issue = parsed_issue_arg.issue
2135 if parsed_issue_arg.hostname:
2136 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2137
skobes6468b902016-10-24 08:45:10 -07002138 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2139 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2140 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002141 try:
skobes6468b902016-10-24 08:45:10 -07002142 scm_obj.apply_patch(patchset_object)
2143 except Exception as e:
2144 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002145 return 1
2146
2147 # If we had an issue, commit the current state and register the issue.
2148 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002149 self.SetIssue(self.GetIssue())
2150 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002151 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2152 'patch from issue %(i)s at patchset '
2153 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2154 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002155 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002156 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002157 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002158 return 0
2159
2160 @staticmethod
2161 def ParseIssueURL(parsed_url):
2162 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2163 return None
wychen3c1c1722016-08-04 11:46:36 -07002164 # Rietveld patch: https://domain/<number>/#ps<patchset>
2165 match = re.match(r'/(\d+)/$', parsed_url.path)
2166 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2167 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002168 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002169 issue=int(match.group(1)),
2170 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002171 hostname=parsed_url.netloc,
2172 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002173 # Typical url: https://domain/<issue_number>[/[other]]
2174 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2175 if match:
skobes6468b902016-10-24 08:45:10 -07002176 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002177 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002178 hostname=parsed_url.netloc,
2179 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002180 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2181 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2182 if match:
skobes6468b902016-10-24 08:45:10 -07002183 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002184 issue=int(match.group(1)),
2185 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002186 hostname=parsed_url.netloc,
2187 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002188 return None
2189
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002190 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002191 """Upload the patch to Rietveld."""
2192 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2193 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002194 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2195 if options.emulate_svn_auto_props:
2196 upload_args.append('--emulate_svn_auto_props')
2197
2198 change_desc = None
2199
2200 if options.email is not None:
2201 upload_args.extend(['--email', options.email])
2202
2203 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002204 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002205 upload_args.extend(['--title', options.title])
2206 if options.message:
2207 upload_args.extend(['--message', options.message])
2208 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002209 print('This branch is associated with issue %s. '
2210 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211 else:
nodirca166002016-06-27 10:59:51 -07002212 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002213 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002214 if options.message:
2215 message = options.message
2216 else:
2217 message = CreateDescriptionFromLog(args)
2218 if options.title:
2219 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002221 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002222 change_desc.update_reviewers(options.reviewers, options.tbrs,
2223 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002224 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002225 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002226
2227 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002228 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002229 return 1
2230
2231 upload_args.extend(['--message', change_desc.description])
2232 if change_desc.get_reviewers():
2233 upload_args.append('--reviewers=%s' % ','.join(
2234 change_desc.get_reviewers()))
2235 if options.send_mail:
2236 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002237 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002238 upload_args.append('--send_mail')
2239
2240 # We check this before applying rietveld.private assuming that in
2241 # rietveld.cc only addresses which we can send private CLs to are listed
2242 # if rietveld.private is set, and so we should ignore rietveld.cc only
2243 # when --private is specified explicitly on the command line.
2244 if options.private:
2245 logging.warn('rietveld.cc is ignored since private flag is specified. '
2246 'You need to review and add them manually if necessary.')
2247 cc = self.GetCCListWithoutDefault()
2248 else:
2249 cc = self.GetCCList()
2250 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002251 if change_desc.get_cced():
2252 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002253 if cc:
2254 upload_args.extend(['--cc', cc])
2255
2256 if options.private or settings.GetDefaultPrivateFlag() == "True":
2257 upload_args.append('--private')
2258
2259 upload_args.extend(['--git_similarity', str(options.similarity)])
2260 if not options.find_copies:
2261 upload_args.extend(['--git_no_find_copies'])
2262
2263 # Include the upstream repo's URL in the change -- this is useful for
2264 # projects that have their source spread across multiple repos.
2265 remote_url = self.GetGitBaseUrlFromConfig()
2266 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002267 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2268 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2269 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002270 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002271 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002272 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002273 if target_ref:
2274 upload_args.extend(['--target_ref', target_ref])
2275
2276 # Look for dependent patchsets. See crbug.com/480453 for more details.
2277 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2278 upstream_branch = ShortBranchName(upstream_branch)
2279 if remote is '.':
2280 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002281 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002282 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002283 print()
2284 print('Skipping dependency patchset upload because git config '
2285 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2286 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002287 else:
2288 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002289 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002290 auth_config=auth_config)
2291 branch_cl_issue_url = branch_cl.GetIssueURL()
2292 branch_cl_issue = branch_cl.GetIssue()
2293 branch_cl_patchset = branch_cl.GetPatchset()
2294 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2295 upload_args.extend(
2296 ['--depends_on_patchset', '%s:%s' % (
2297 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002298 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002299 '\n'
2300 'The current branch (%s) is tracking a local branch (%s) with '
2301 'an associated CL.\n'
2302 'Adding %s/#ps%s as a dependency patchset.\n'
2303 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2304 branch_cl_patchset))
2305
2306 project = settings.GetProject()
2307 if project:
2308 upload_args.extend(['--project', project])
2309
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002310 try:
2311 upload_args = ['upload'] + upload_args + args
2312 logging.info('upload.RealMain(%s)', upload_args)
2313 issue, patchset = upload.RealMain(upload_args)
2314 issue = int(issue)
2315 patchset = int(patchset)
2316 except KeyboardInterrupt:
2317 sys.exit(1)
2318 except:
2319 # If we got an exception after the user typed a description for their
2320 # change, back up the description before re-raising.
2321 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002322 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002323 raise
2324
2325 if not self.GetIssue():
2326 self.SetIssue(issue)
2327 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002328 return 0
2329
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002330
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002331class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002332 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002333 # auth_config is Rietveld thing, kept here to preserve interface only.
2334 super(_GerritChangelistImpl, self).__init__(changelist)
2335 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002336 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002337 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002338 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002339 # Map from change number (issue) to its detail cache.
2340 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002341
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002342 if codereview_host is not None:
2343 assert not codereview_host.startswith('https://'), codereview_host
2344 self._gerrit_host = codereview_host
2345 self._gerrit_server = 'https://%s' % codereview_host
2346
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002347 def _GetGerritHost(self):
2348 # Lazy load of configs.
2349 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002350 if self._gerrit_host and '.' not in self._gerrit_host:
2351 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2352 # This happens for internal stuff http://crbug.com/614312.
2353 parsed = urlparse.urlparse(self.GetRemoteUrl())
2354 if parsed.scheme == 'sso':
2355 print('WARNING: using non https URLs for remote is likely broken\n'
2356 ' Your current remote is: %s' % self.GetRemoteUrl())
2357 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2358 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002359 return self._gerrit_host
2360
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002361 def _GetGitHost(self):
2362 """Returns git host to be used when uploading change to Gerrit."""
2363 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2364
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002365 def GetCodereviewServer(self):
2366 if not self._gerrit_server:
2367 # If we're on a branch then get the server potentially associated
2368 # with that branch.
2369 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002370 self._gerrit_server = self._GitGetBranchConfigValue(
2371 self.CodereviewServerConfigKey())
2372 if self._gerrit_server:
2373 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002374 if not self._gerrit_server:
2375 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2376 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002377 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002378 parts[0] = parts[0] + '-review'
2379 self._gerrit_host = '.'.join(parts)
2380 self._gerrit_server = 'https://%s' % self._gerrit_host
2381 return self._gerrit_server
2382
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002383 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002384 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002385 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002386
tandrii5d48c322016-08-18 16:19:37 -07002387 @classmethod
2388 def PatchsetConfigKey(cls):
2389 return 'gerritpatchset'
2390
2391 @classmethod
2392 def CodereviewServerConfigKey(cls):
2393 return 'gerritserver'
2394
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002395 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002396 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002397 if settings.GetGerritSkipEnsureAuthenticated():
2398 # For projects with unusual authentication schemes.
2399 # See http://crbug.com/603378.
2400 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002401 # Lazy-loader to identify Gerrit and Git hosts.
2402 if gerrit_util.GceAuthenticator.is_gce():
2403 return
2404 self.GetCodereviewServer()
2405 git_host = self._GetGitHost()
2406 assert self._gerrit_server and self._gerrit_host
2407 cookie_auth = gerrit_util.CookiesAuthenticator()
2408
2409 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2410 git_auth = cookie_auth.get_auth_header(git_host)
2411 if gerrit_auth and git_auth:
2412 if gerrit_auth == git_auth:
2413 return
2414 print((
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002415 'WARNING: you have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002416 ' %s\n'
2417 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002418 ' Consider running the following command:\n'
2419 ' git cl creds-check\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002420 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002421 (git_host, self._gerrit_host,
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002422 cookie_auth.get_new_password_message(git_host)))
2423 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002424 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002425 return
2426 else:
2427 missing = (
2428 [] if gerrit_auth else [self._gerrit_host] +
2429 [] if git_auth else [git_host])
2430 DieWithError('Credentials for the following hosts are required:\n'
2431 ' %s\n'
2432 'These are read from %s (or legacy %s)\n'
2433 '%s' % (
2434 '\n '.join(missing),
2435 cookie_auth.get_gitcookies_path(),
2436 cookie_auth.get_netrc_path(),
2437 cookie_auth.get_new_password_message(git_host)))
2438
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002439 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002440 if not self.GetIssue():
2441 return
2442
2443 # Warm change details cache now to avoid RPCs later, reducing latency for
2444 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002445 self._GetChangeDetail(
2446 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002447
2448 status = self._GetChangeDetail()['status']
2449 if status in ('MERGED', 'ABANDONED'):
2450 DieWithError('Change %s has been %s, new uploads are not allowed' %
2451 (self.GetIssueURL(),
2452 'submitted' if status == 'MERGED' else 'abandoned'))
2453
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002454 if gerrit_util.GceAuthenticator.is_gce():
2455 return
2456 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2457 self._GetGerritHost())
2458 if self.GetIssueOwner() == cookies_user:
2459 return
2460 logging.debug('change %s owner is %s, cookies user is %s',
2461 self.GetIssue(), self.GetIssueOwner(), cookies_user)
2462 # Maybe user has linked accounts or smth like that,
2463 # so ask what Gerrit thinks of this user.
2464 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2465 if details['email'] == self.GetIssueOwner():
2466 return
2467 if not force:
2468 print('WARNING: change %s is owned by %s, but you authenticate to Gerrit '
2469 'as %s.\n'
2470 'Uploading may fail due to lack of permissions.' %
2471 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2472 confirm_or_exit(action='upload')
2473
2474
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002475 def _PostUnsetIssueProperties(self):
2476 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002477 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002478
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002479 def GetRieveldObjForPresubmit(self):
2480 class ThisIsNotRietveldIssue(object):
2481 def __nonzero__(self):
2482 # This is a hack to make presubmit_support think that rietveld is not
2483 # defined, yet still ensure that calls directly result in a decent
2484 # exception message below.
2485 return False
2486
2487 def __getattr__(self, attr):
2488 print(
2489 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2490 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2491 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2492 'or use Rietveld for codereview.\n'
2493 'See also http://crbug.com/579160.' % attr)
2494 raise NotImplementedError()
2495 return ThisIsNotRietveldIssue()
2496
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002497 def GetGerritObjForPresubmit(self):
2498 return presubmit_support.GerritAccessor(self._GetGerritHost())
2499
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002500 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002501 """Apply a rough heuristic to give a simple summary of an issue's review
2502 or CQ status, assuming adherence to a common workflow.
2503
2504 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002505 * 'error' - error from review tool (including deleted issues)
2506 * 'unsent' - no reviewers added
2507 * 'waiting' - waiting for review
2508 * 'reply' - waiting for uploader to reply to review
2509 * 'lgtm' - Code-Review label has been set
2510 * 'commit' - in the commit queue
2511 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002512 """
2513 if not self.GetIssue():
2514 return None
2515
2516 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002517 data = self._GetChangeDetail([
2518 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002519 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002520 return 'error'
2521
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002522 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002523 return 'closed'
2524
Aaron Gable9ab38c62017-04-06 14:36:33 -07002525 if data['labels'].get('Commit-Queue', {}).get('approved'):
2526 # The section will have an "approved" subsection if anyone has voted
2527 # the maximum value on the label.
2528 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002529
Aaron Gable9ab38c62017-04-06 14:36:33 -07002530 if data['labels'].get('Code-Review', {}).get('approved'):
2531 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002532
2533 if not data.get('reviewers', {}).get('REVIEWER', []):
2534 return 'unsent'
2535
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002536 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002537 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2538 last_message_author = messages.pop().get('author', {})
2539 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002540 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2541 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002542 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002543 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002544 if last_message_author.get('_account_id') == owner:
2545 # Most recent message was by owner.
2546 return 'waiting'
2547 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002548 # Some reply from non-owner.
2549 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002550
2551 # Somehow there are no messages even though there are reviewers.
2552 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002553
2554 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002555 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002556 return data['revisions'][data['current_revision']]['_number']
2557
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002558 def FetchDescription(self, force=False):
2559 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2560 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002561 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002562 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002563
dsansomee2d6fd92016-09-08 00:10:47 -07002564 def UpdateDescriptionRemote(self, description, force=False):
2565 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2566 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002567 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002568 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002569 'unpublished edit. Either publish the edit in the Gerrit web UI '
2570 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002571
2572 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2573 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002574 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002575 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002576
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002577 def AddComment(self, message):
2578 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2579 msg=message)
2580
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002581 def GetCommentsSummary(self):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002582 # DETAILED_ACCOUNTS is to get emails in accounts.
2583 data = self._GetChangeDetail(options=['MESSAGES', 'DETAILED_ACCOUNTS'])
2584 summary = []
2585 for msg in data.get('messages', []):
2586 # Gerrit spits out nanoseconds.
2587 assert len(msg['date'].split('.')[-1]) == 9
2588 date = datetime.datetime.strptime(msg['date'][:-3],
2589 '%Y-%m-%d %H:%M:%S.%f')
2590 summary.append(_CommentSummary(
2591 date=date,
2592 message=msg['message'],
2593 sender=msg['author']['email'],
2594 # These could be inferred from the text messages and correlated with
2595 # Code-Review label maximum, however this is not reliable.
2596 # Leaving as is until the need arises.
2597 approval=False,
2598 disapproval=False,
2599 ))
2600 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002601
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002602 def CloseIssue(self):
2603 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2604
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002605 def SubmitIssue(self, wait_for_merge=True):
2606 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2607 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002608
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002609 def _GetChangeDetail(self, options=None, issue=None,
2610 no_cache=False):
2611 """Returns details of the issue by querying Gerrit and caching results.
2612
2613 If fresh data is needed, set no_cache=True which will clear cache and
2614 thus new data will be fetched from Gerrit.
2615 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002616 options = options or []
2617 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002618 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002619
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002620 # Optimization to avoid multiple RPCs:
2621 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2622 'CURRENT_COMMIT' not in options):
2623 options.append('CURRENT_COMMIT')
2624
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002625 # Normalize issue and options for consistent keys in cache.
2626 issue = str(issue)
2627 options = [o.upper() for o in options]
2628
2629 # Check in cache first unless no_cache is True.
2630 if no_cache:
2631 self._detail_cache.pop(issue, None)
2632 else:
2633 options_set = frozenset(options)
2634 for cached_options_set, data in self._detail_cache.get(issue, []):
2635 # Assumption: data fetched before with extra options is suitable
2636 # for return for a smaller set of options.
2637 # For example, if we cached data for
2638 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2639 # and request is for options=[CURRENT_REVISION],
2640 # THEN we can return prior cached data.
2641 if options_set.issubset(cached_options_set):
2642 return data
2643
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002644 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002645 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002646 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002647 except gerrit_util.GerritError as e:
2648 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002649 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002650 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002651
2652 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002653 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002654
agable32978d92016-11-01 12:55:02 -07002655 def _GetChangeCommit(self, issue=None):
2656 issue = issue or self.GetIssue()
2657 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002658 try:
2659 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2660 except gerrit_util.GerritError as e:
2661 if e.http_status == 404:
2662 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2663 raise
agable32978d92016-11-01 12:55:02 -07002664 return data
2665
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002666 def CMDLand(self, force, bypass_hooks, verbose):
2667 if git_common.is_dirty_git_tree('land'):
2668 return 1
tandriid60367b2016-06-22 05:25:12 -07002669 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2670 if u'Commit-Queue' in detail.get('labels', {}):
2671 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002672 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2673 'which can test and land changes for you. '
2674 'Are you sure you wish to bypass it?\n',
2675 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002676
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002677 differs = True
tandriic4344b52016-08-29 06:04:54 -07002678 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002679 # Note: git diff outputs nothing if there is no diff.
2680 if not last_upload or RunGit(['diff', last_upload]).strip():
2681 print('WARNING: some changes from local branch haven\'t been uploaded')
2682 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002683 if detail['current_revision'] == last_upload:
2684 differs = False
2685 else:
2686 print('WARNING: local branch contents differ from latest uploaded '
2687 'patchset')
2688 if differs:
2689 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002690 confirm_or_exit(
2691 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2692 action='submit')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002693 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2694 elif not bypass_hooks:
2695 hook_results = self.RunHook(
2696 committing=True,
2697 may_prompt=not force,
2698 verbose=verbose,
2699 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2700 if not hook_results.should_continue():
2701 return 1
2702
2703 self.SubmitIssue(wait_for_merge=True)
2704 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002705 links = self._GetChangeCommit().get('web_links', [])
2706 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002707 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002708 print('Landed as %s' % link.get('url'))
2709 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002710 return 0
2711
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002712 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2713 directory):
2714 assert not reject
2715 assert not nocommit
2716 assert not directory
2717 assert parsed_issue_arg.valid
2718
2719 self._changelist.issue = parsed_issue_arg.issue
2720
2721 if parsed_issue_arg.hostname:
2722 self._gerrit_host = parsed_issue_arg.hostname
2723 self._gerrit_server = 'https://%s' % self._gerrit_host
2724
tandriic2405f52016-10-10 08:13:15 -07002725 try:
2726 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002727 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002728 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002729
2730 if not parsed_issue_arg.patchset:
2731 # Use current revision by default.
2732 revision_info = detail['revisions'][detail['current_revision']]
2733 patchset = int(revision_info['_number'])
2734 else:
2735 patchset = parsed_issue_arg.patchset
2736 for revision_info in detail['revisions'].itervalues():
2737 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2738 break
2739 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002740 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002741 (parsed_issue_arg.patchset, self.GetIssue()))
2742
2743 fetch_info = revision_info['fetch']['http']
2744 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002745 self.SetIssue(self.GetIssue())
2746 self.SetPatchset(patchset)
Aaron Gabled343c632017-03-15 11:02:26 -07002747 RunGit(['cherry-pick', 'FETCH_HEAD'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002748 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002749 (self.GetIssue(), self.GetPatchset()))
2750 return 0
2751
2752 @staticmethod
2753 def ParseIssueURL(parsed_url):
2754 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2755 return None
2756 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2757 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2758 # Short urls like https://domain/<issue_number> can be used, but don't allow
2759 # specifying the patchset (you'd 404), but we allow that here.
2760 if parsed_url.path == '/':
2761 part = parsed_url.fragment
2762 else:
2763 part = parsed_url.path
2764 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2765 if match:
2766 return _ParsedIssueNumberArgument(
2767 issue=int(match.group(2)),
2768 patchset=int(match.group(4)) if match.group(4) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002769 hostname=parsed_url.netloc,
2770 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002771 return None
2772
tandrii16e0b4e2016-06-07 10:34:28 -07002773 def _GerritCommitMsgHookCheck(self, offer_removal):
2774 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2775 if not os.path.exists(hook):
2776 return
2777 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2778 # custom developer made one.
2779 data = gclient_utils.FileRead(hook)
2780 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2781 return
2782 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002783 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002784 'and may interfere with it in subtle ways.\n'
2785 'We recommend you remove the commit-msg hook.')
2786 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002787 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002788 gclient_utils.rm_file_or_tree(hook)
2789 print('Gerrit commit-msg hook removed.')
2790 else:
2791 print('OK, will keep Gerrit commit-msg hook in place.')
2792
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002793 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002794 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002795 if options.squash and options.no_squash:
2796 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002797
2798 if not options.squash and not options.no_squash:
2799 # Load default for user, repo, squash=true, in this order.
2800 options.squash = settings.GetSquashGerritUploads()
2801 elif options.no_squash:
2802 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002803
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002804 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002805 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002806
Aaron Gableb56ad332017-01-06 15:24:31 -08002807 # This may be None; default fallback value is determined in logic below.
2808 title = options.title
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002809 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002810
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002811 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002812 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002813 if self.GetIssue():
2814 # Try to get the message from a previous upload.
2815 message = self.GetDescription()
2816 if not message:
2817 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002818 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002819 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002820 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002821 if options.message:
2822 # For compatibility with Rietveld, if -m|--message is given on
2823 # command line, title should be the first line of that message,
2824 # which shouldn't be confused with CL description.
2825 default_title = options.message.strip().split()[0]
2826 else:
2827 default_title = RunGit(
2828 ['show', '-s', '--format=%s', 'HEAD']).strip()
Andrii Shyshkalove00a29b2017-04-10 14:48:28 +02002829 if options.force:
2830 title = default_title
2831 else:
2832 title = ask_for_data(
2833 'Title for patchset [%s]: ' % default_title) or default_title
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002834 if title == default_title:
2835 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002836 change_id = self._GetChangeDetail()['change_id']
2837 while True:
2838 footer_change_ids = git_footers.get_footer_change_id(message)
2839 if footer_change_ids == [change_id]:
2840 break
2841 if not footer_change_ids:
2842 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002843 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002844 continue
2845 # There is already a valid footer but with different or several ids.
2846 # Doing this automatically is non-trivial as we don't want to lose
2847 # existing other footers, yet we want to append just 1 desired
2848 # Change-Id. Thus, just create a new footer, but let user verify the
2849 # new description.
2850 message = '%s\n\nChange-Id: %s' % (message, change_id)
2851 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002852 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002853 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002854 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002855 'Please, check the proposed correction to the description, '
2856 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2857 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2858 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002859 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002860 if not options.force:
2861 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002862 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002863 message = change_desc.description
2864 if not message:
2865 DieWithError("Description is empty. Aborting...")
2866 # Continue the while loop.
2867 # Sanity check of this code - we should end up with proper message
2868 # footer.
2869 assert [change_id] == git_footers.get_footer_change_id(message)
2870 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002871 else: # if not self.GetIssue()
2872 if options.message:
2873 message = options.message
2874 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002875 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002876 if options.title:
2877 message = options.title + '\n\n' + message
2878 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002879
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002880 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002881 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002882 # On first upload, patchset title is always this string, while
2883 # --title flag gets converted to first line of message.
2884 title = 'Initial upload'
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002885 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002886 if not change_desc.description:
2887 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002888 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002889 if len(change_ids) > 1:
2890 DieWithError('too many Change-Id footers, at most 1 allowed.')
2891 if not change_ids:
2892 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002893 change_desc.set_description(git_footers.add_footer_change_id(
2894 change_desc.description,
2895 GenerateGerritChangeId(change_desc.description)))
2896 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002897 assert len(change_ids) == 1
2898 change_id = change_ids[0]
2899
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002900 if options.reviewers or options.tbrs or options.add_owners_to:
2901 change_desc.update_reviewers(options.reviewers, options.tbrs,
2902 options.add_owners_to, change)
2903
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002904 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002905 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2906 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002907 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2908 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002909 '-m', change_desc.description]).strip()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002910 else:
2911 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002912 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002913 if not change_desc.description:
2914 DieWithError("Description is empty. Aborting...")
2915
2916 if not git_footers.get_footer_change_id(change_desc.description):
2917 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002918 change_desc.set_description(
2919 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002920 if options.reviewers or options.tbrs or options.add_owners_to:
2921 change_desc.update_reviewers(options.reviewers, options.tbrs,
2922 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002923 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002924 # For no-squash mode, we assume the remote called "origin" is the one we
2925 # want. It is not worthwhile to support different workflows for
2926 # no-squash mode.
2927 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002928 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2929
2930 assert change_desc
2931 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2932 ref_to_push)]).splitlines()
2933 if len(commits) > 1:
2934 print('WARNING: This will upload %d commits. Run the following command '
2935 'to see which commits will be uploaded: ' % len(commits))
2936 print('git log %s..%s' % (parent, ref_to_push))
2937 print('You can also use `git squash-branch` to squash these into a '
2938 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002939 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002940
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002941 if options.reviewers or options.tbrs or options.add_owners_to:
2942 change_desc.update_reviewers(options.reviewers, options.tbrs,
2943 options.add_owners_to, change)
2944
2945 if options.send_mail:
2946 if not change_desc.get_reviewers():
2947 DieWithError('Must specify reviewers to send email.', change_desc)
2948
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002949 # Extra options that can be specified at push time. Doc:
2950 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002951 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002952 if change_desc.get_reviewers(tbr_only=True):
2953 print('Adding self-LGTM (Code-Review +1) because of TBRs')
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002954 refspec_opts.append('l=Code-Review+1')
tandrii99a72f22016-08-17 14:33:24 -07002955
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002956
2957 # TODO(tandrii): options.message should be posted as a comment
2958 # if --send-email is set on non-initial upload as Rietveld used to do it.
2959
Aaron Gable9b713dd2016-12-14 16:04:21 -08002960 if title:
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002961 if not re.match(r'^[\w ]+$', title):
2962 title = re.sub(r'[^\w ]', '', title)
2963 if not automatic_title:
2964 print('WARNING: Patchset title may only contain alphanumeric chars '
2965 'and spaces. You can edit it in the UI. '
2966 'See https://crbug.com/663787.\n'
2967 'Cleaned up title: %s' % title)
2968 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2969 # reverse on its side.
2970 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002971
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002972 # Never notify now because no one is on the review. Notify when we add
2973 # reviewers and CCs below.
2974 refspec_opts.append('notify=NONE')
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002975
agablec6787972016-09-09 16:13:34 -07002976 if options.private:
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002977 refspec_opts.append('draft')
agablec6787972016-09-09 16:13:34 -07002978
rmistry9eadede2016-09-19 11:22:43 -07002979 if options.topic:
2980 # Documentation on Gerrit topics is here:
2981 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002982 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002983
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002984 refspec_suffix = ''
2985 if refspec_opts:
2986 refspec_suffix = '%' + ','.join(refspec_opts)
2987 assert ' ' not in refspec_suffix, (
2988 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2989 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2990
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002991 try:
2992 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002993 ['git', 'push', self.GetRemoteUrl(), refspec],
2994 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002995 # Flush after every line: useful for seeing progress when running as
2996 # recipe.
2997 filter_fn=lambda _: sys.stdout.flush())
2998 except subprocess2.CalledProcessError:
2999 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003000 'for the reason of the failure.\n'
3001 'Hint: run command below to diangose common Git/Gerrit '
3002 'credential problems:\n'
3003 ' git cl creds-check\n',
3004 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003005
3006 if options.squash:
3007 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
3008 change_numbers = [m.group(1)
3009 for m in map(regex.match, push_stdout.splitlines())
3010 if m]
3011 if len(change_numbers) != 1:
3012 DieWithError(
3013 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003014 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003015 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003016 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003017
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003018 reviewers = sorted(change_desc.get_reviewers())
3019
tandrii88189772016-09-29 04:29:57 -07003020 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003021 if not options.private:
3022 cc = self.GetCCList().split(',')
3023 else:
3024 cc = []
tandrii88189772016-09-29 04:29:57 -07003025 if options.cc:
3026 cc.extend(options.cc)
3027 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003028 if change_desc.get_cced():
3029 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003030
3031 gerrit_util.AddReviewers(
3032 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3033 notify=bool(options.send_mail))
3034
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003035 return 0
3036
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003037 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3038 change_desc):
3039 """Computes parent of the generated commit to be uploaded to Gerrit.
3040
3041 Returns revision or a ref name.
3042 """
3043 if custom_cl_base:
3044 # Try to avoid creating additional unintended CLs when uploading, unless
3045 # user wants to take this risk.
3046 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3047 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3048 local_ref_of_target_remote])
3049 if code == 1:
3050 print('\nWARNING: manually specified base of this CL `%s` '
3051 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3052 'If you proceed with upload, more than 1 CL may be created by '
3053 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3054 'If you are certain that specified base `%s` has already been '
3055 'uploaded to Gerrit as another CL, you may proceed.\n' %
3056 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3057 if not force:
3058 confirm_or_exit(
3059 'Do you take responsibility for cleaning up potential mess '
3060 'resulting from proceeding with upload?',
3061 action='upload')
3062 return custom_cl_base
3063
Aaron Gablef97e33d2017-03-30 15:44:27 -07003064 if remote != '.':
3065 return self.GetCommonAncestorWithUpstream()
3066
3067 # If our upstream branch is local, we base our squashed commit on its
3068 # squashed version.
3069 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3070
Aaron Gablef97e33d2017-03-30 15:44:27 -07003071 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003072 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003073
3074 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003075 # TODO(tandrii): consider checking parent change in Gerrit and using its
3076 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3077 # the tree hash of the parent branch. The upside is less likely bogus
3078 # requests to reupload parent change just because it's uploadhash is
3079 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003080 parent = RunGit(['config',
3081 'branch.%s.gerritsquashhash' % upstream_branch_name],
3082 error_ok=True).strip()
3083 # Verify that the upstream branch has been uploaded too, otherwise
3084 # Gerrit will create additional CLs when uploading.
3085 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3086 RunGitSilent(['rev-parse', parent + ':'])):
3087 DieWithError(
3088 '\nUpload upstream branch %s first.\n'
3089 'It is likely that this branch has been rebased since its last '
3090 'upload, so you just need to upload it again.\n'
3091 '(If you uploaded it with --no-squash, then branch dependencies '
3092 'are not supported, and you should reupload with --squash.)'
3093 % upstream_branch_name,
3094 change_desc)
3095 return parent
3096
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003097 def _AddChangeIdToCommitMessage(self, options, args):
3098 """Re-commits using the current message, assumes the commit hook is in
3099 place.
3100 """
3101 log_desc = options.message or CreateDescriptionFromLog(args)
3102 git_command = ['commit', '--amend', '-m', log_desc]
3103 RunGit(git_command)
3104 new_log_desc = CreateDescriptionFromLog(args)
3105 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003106 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003107 return new_log_desc
3108 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003109 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003110
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003111 def SetCQState(self, new_state):
3112 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003113 vote_map = {
3114 _CQState.NONE: 0,
3115 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003116 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003117 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01003118 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
3119 if new_state == _CQState.DRY_RUN:
3120 # Don't spam everybody reviewer/owner.
3121 kwargs['notify'] = 'NONE'
3122 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003123
tandriie113dfd2016-10-11 10:20:12 -07003124 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003125 try:
3126 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003127 except GerritChangeNotExists:
3128 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003129
3130 if data['status'] in ('ABANDONED', 'MERGED'):
3131 return 'CL %s is closed' % self.GetIssue()
3132
3133 def GetTryjobProperties(self, patchset=None):
3134 """Returns dictionary of properties to launch tryjob."""
3135 data = self._GetChangeDetail(['ALL_REVISIONS'])
3136 patchset = int(patchset or self.GetPatchset())
3137 assert patchset
3138 revision_data = None # Pylint wants it to be defined.
3139 for revision_data in data['revisions'].itervalues():
3140 if int(revision_data['_number']) == patchset:
3141 break
3142 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003143 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003144 (patchset, self.GetIssue()))
3145 return {
3146 'patch_issue': self.GetIssue(),
3147 'patch_set': patchset or self.GetPatchset(),
3148 'patch_project': data['project'],
3149 'patch_storage': 'gerrit',
3150 'patch_ref': revision_data['fetch']['http']['ref'],
3151 'patch_repository_url': revision_data['fetch']['http']['url'],
3152 'patch_gerrit_url': self.GetCodereviewServer(),
3153 }
tandriie113dfd2016-10-11 10:20:12 -07003154
tandriide281ae2016-10-12 06:02:30 -07003155 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003156 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003157
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003158
3159_CODEREVIEW_IMPLEMENTATIONS = {
3160 'rietveld': _RietveldChangelistImpl,
3161 'gerrit': _GerritChangelistImpl,
3162}
3163
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003164
iannuccie53c9352016-08-17 14:40:40 -07003165def _add_codereview_issue_select_options(parser, extra=""):
3166 _add_codereview_select_options(parser)
3167
3168 text = ('Operate on this issue number instead of the current branch\'s '
3169 'implicit issue.')
3170 if extra:
3171 text += ' '+extra
3172 parser.add_option('-i', '--issue', type=int, help=text)
3173
3174
3175def _process_codereview_issue_select_options(parser, options):
3176 _process_codereview_select_options(parser, options)
3177 if options.issue is not None and not options.forced_codereview:
3178 parser.error('--issue must be specified with either --rietveld or --gerrit')
3179
3180
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003181def _add_codereview_select_options(parser):
3182 """Appends --gerrit and --rietveld options to force specific codereview."""
3183 parser.codereview_group = optparse.OptionGroup(
3184 parser, 'EXPERIMENTAL! Codereview override options')
3185 parser.add_option_group(parser.codereview_group)
3186 parser.codereview_group.add_option(
3187 '--gerrit', action='store_true',
3188 help='Force the use of Gerrit for codereview')
3189 parser.codereview_group.add_option(
3190 '--rietveld', action='store_true',
3191 help='Force the use of Rietveld for codereview')
3192
3193
3194def _process_codereview_select_options(parser, options):
3195 if options.gerrit and options.rietveld:
3196 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3197 options.forced_codereview = None
3198 if options.gerrit:
3199 options.forced_codereview = 'gerrit'
3200 elif options.rietveld:
3201 options.forced_codereview = 'rietveld'
3202
3203
tandriif9aefb72016-07-01 09:06:51 -07003204def _get_bug_line_values(default_project, bugs):
3205 """Given default_project and comma separated list of bugs, yields bug line
3206 values.
3207
3208 Each bug can be either:
3209 * a number, which is combined with default_project
3210 * string, which is left as is.
3211
3212 This function may produce more than one line, because bugdroid expects one
3213 project per line.
3214
3215 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3216 ['v8:123', 'chromium:789']
3217 """
3218 default_bugs = []
3219 others = []
3220 for bug in bugs.split(','):
3221 bug = bug.strip()
3222 if bug:
3223 try:
3224 default_bugs.append(int(bug))
3225 except ValueError:
3226 others.append(bug)
3227
3228 if default_bugs:
3229 default_bugs = ','.join(map(str, default_bugs))
3230 if default_project:
3231 yield '%s:%s' % (default_project, default_bugs)
3232 else:
3233 yield default_bugs
3234 for other in sorted(others):
3235 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3236 yield other
3237
3238
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003239class ChangeDescription(object):
3240 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003241 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003242 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003243 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003244 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003245
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003246 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003247 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003248
agable@chromium.org42c20792013-09-12 17:34:49 +00003249 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003250 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003251 return '\n'.join(self._description_lines)
3252
3253 def set_description(self, desc):
3254 if isinstance(desc, basestring):
3255 lines = desc.splitlines()
3256 else:
3257 lines = [line.rstrip() for line in desc]
3258 while lines and not lines[0]:
3259 lines.pop(0)
3260 while lines and not lines[-1]:
3261 lines.pop(-1)
3262 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003263
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003264 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3265 """Rewrites the R=/TBR= line(s) as a single line each.
3266
3267 Args:
3268 reviewers (list(str)) - list of additional emails to use for reviewers.
3269 tbrs (list(str)) - list of additional emails to use for TBRs.
3270 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3271 the change that are missing OWNER coverage. If this is not None, you
3272 must also pass a value for `change`.
3273 change (Change) - The Change that should be used for OWNERS lookups.
3274 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003275 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003276 assert isinstance(tbrs, list), tbrs
3277
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003278 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003279 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003280
3281 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003282 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003283
3284 reviewers = set(reviewers)
3285 tbrs = set(tbrs)
3286 LOOKUP = {
3287 'TBR': tbrs,
3288 'R': reviewers,
3289 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003290
agable@chromium.org42c20792013-09-12 17:34:49 +00003291 # Get the set of R= and TBR= lines and remove them from the desciption.
3292 regexp = re.compile(self.R_LINE)
3293 matches = [regexp.match(line) for line in self._description_lines]
3294 new_desc = [l for i, l in enumerate(self._description_lines)
3295 if not matches[i]]
3296 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003297
agable@chromium.org42c20792013-09-12 17:34:49 +00003298 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003299
3300 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003301 for match in matches:
3302 if not match:
3303 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003304 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3305
3306 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003307 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003308 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003309 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003310 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003311 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003312 LOOKUP[add_owners_to].update(
3313 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003314
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003315 # If any folks ended up in both groups, remove them from tbrs.
3316 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003317
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003318 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3319 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003320
3321 # Put the new lines in the description where the old first R= line was.
3322 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3323 if 0 <= line_loc < len(self._description_lines):
3324 if new_tbr_line:
3325 self._description_lines.insert(line_loc, new_tbr_line)
3326 if new_r_line:
3327 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003328 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003329 if new_r_line:
3330 self.append_footer(new_r_line)
3331 if new_tbr_line:
3332 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003333
Aaron Gable3a16ed12017-03-23 10:51:55 -07003334 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003335 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003336 self.set_description([
3337 '# Enter a description of the change.',
3338 '# This will be displayed on the codereview site.',
3339 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003340 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003341 '--------------------',
3342 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003343
agable@chromium.org42c20792013-09-12 17:34:49 +00003344 regexp = re.compile(self.BUG_LINE)
3345 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003346 prefix = settings.GetBugPrefix()
3347 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003348 if git_footer:
3349 self.append_footer('Bug: %s' % ', '.join(values))
3350 else:
3351 for value in values:
3352 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003353
agable@chromium.org42c20792013-09-12 17:34:49 +00003354 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003355 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003356 if not content:
3357 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003358 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003359
3360 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003361 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3362 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003363 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003364 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003365
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003366 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003367 """Adds a footer line to the description.
3368
3369 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3370 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3371 that Gerrit footers are always at the end.
3372 """
3373 parsed_footer_line = git_footers.parse_footer(line)
3374 if parsed_footer_line:
3375 # Line is a gerrit footer in the form: Footer-Key: any value.
3376 # Thus, must be appended observing Gerrit footer rules.
3377 self.set_description(
3378 git_footers.add_footer(self.description,
3379 key=parsed_footer_line[0],
3380 value=parsed_footer_line[1]))
3381 return
3382
3383 if not self._description_lines:
3384 self._description_lines.append(line)
3385 return
3386
3387 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3388 if gerrit_footers:
3389 # git_footers.split_footers ensures that there is an empty line before
3390 # actual (gerrit) footers, if any. We have to keep it that way.
3391 assert top_lines and top_lines[-1] == ''
3392 top_lines, separator = top_lines[:-1], top_lines[-1:]
3393 else:
3394 separator = [] # No need for separator if there are no gerrit_footers.
3395
3396 prev_line = top_lines[-1] if top_lines else ''
3397 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3398 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3399 top_lines.append('')
3400 top_lines.append(line)
3401 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003402
tandrii99a72f22016-08-17 14:33:24 -07003403 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003404 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003405 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003406 reviewers = [match.group(2).strip()
3407 for match in matches
3408 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003409 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003410
bradnelsond975b302016-10-23 12:20:23 -07003411 def get_cced(self):
3412 """Retrieves the list of reviewers."""
3413 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3414 cced = [match.group(2).strip() for match in matches if match]
3415 return cleanup_list(cced)
3416
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003417 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3418 """Updates this commit description given the parent.
3419
3420 This is essentially what Gnumbd used to do.
3421 Consult https://goo.gl/WMmpDe for more details.
3422 """
3423 assert parent_msg # No, orphan branch creation isn't supported.
3424 assert parent_hash
3425 assert dest_ref
3426 parent_footer_map = git_footers.parse_footers(parent_msg)
3427 # This will also happily parse svn-position, which GnumbD is no longer
3428 # supporting. While we'd generate correct footers, the verifier plugin
3429 # installed in Gerrit will block such commit (ie git push below will fail).
3430 parent_position = git_footers.get_position(parent_footer_map)
3431
3432 # Cherry-picks may have last line obscuring their prior footers,
3433 # from git_footers perspective. This is also what Gnumbd did.
3434 cp_line = None
3435 if (self._description_lines and
3436 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3437 cp_line = self._description_lines.pop()
3438
3439 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3440
3441 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3442 # user interference with actual footers we'd insert below.
3443 for i, (k, v) in enumerate(parsed_footers):
3444 if k.startswith('Cr-'):
3445 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3446
3447 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003448 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003449 if parent_position[0] == dest_ref:
3450 # Same branch as parent.
3451 number = int(parent_position[1]) + 1
3452 else:
3453 number = 1 # New branch, and extra lineage.
3454 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3455 int(parent_position[1])))
3456
3457 parsed_footers.append(('Cr-Commit-Position',
3458 '%s@{#%d}' % (dest_ref, number)))
3459 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3460
3461 self._description_lines = top_lines
3462 if cp_line:
3463 self._description_lines.append(cp_line)
3464 if self._description_lines[-1] != '':
3465 self._description_lines.append('') # Ensure footer separator.
3466 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3467
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003468
Aaron Gablea1bab272017-04-11 16:38:18 -07003469def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003470 """Retrieves the reviewers that approved a CL from the issue properties with
3471 messages.
3472
3473 Note that the list may contain reviewers that are not committer, thus are not
3474 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003475
3476 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003477 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003478 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003479 return sorted(
3480 set(
3481 message['sender']
3482 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003483 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003484 )
3485 )
3486
3487
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003488def FindCodereviewSettingsFile(filename='codereview.settings'):
3489 """Finds the given file starting in the cwd and going up.
3490
3491 Only looks up to the top of the repository unless an
3492 'inherit-review-settings-ok' file exists in the root of the repository.
3493 """
3494 inherit_ok_file = 'inherit-review-settings-ok'
3495 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003496 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003497 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3498 root = '/'
3499 while True:
3500 if filename in os.listdir(cwd):
3501 if os.path.isfile(os.path.join(cwd, filename)):
3502 return open(os.path.join(cwd, filename))
3503 if cwd == root:
3504 break
3505 cwd = os.path.dirname(cwd)
3506
3507
3508def LoadCodereviewSettingsFromFile(fileobj):
3509 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003510 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003511
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003512 def SetProperty(name, setting, unset_error_ok=False):
3513 fullname = 'rietveld.' + name
3514 if setting in keyvals:
3515 RunGit(['config', fullname, keyvals[setting]])
3516 else:
3517 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3518
tandrii48df5812016-10-17 03:55:37 -07003519 if not keyvals.get('GERRIT_HOST', False):
3520 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003521 # Only server setting is required. Other settings can be absent.
3522 # In that case, we ignore errors raised during option deletion attempt.
3523 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003524 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003525 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3526 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003527 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003528 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3529 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003530 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003531 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3532 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003533
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003534 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003535 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003536
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003537 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003538 RunGit(['config', 'gerrit.squash-uploads',
3539 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003540
tandrii@chromium.org28253532016-04-14 13:46:56 +00003541 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003542 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003543 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3544
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003545 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003546 # should be of the form
3547 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3548 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003549 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3550 keyvals['ORIGIN_URL_CONFIG']])
3551
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003552
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003553def urlretrieve(source, destination):
3554 """urllib is broken for SSL connections via a proxy therefore we
3555 can't use urllib.urlretrieve()."""
3556 with open(destination, 'w') as f:
3557 f.write(urllib2.urlopen(source).read())
3558
3559
ukai@chromium.org712d6102013-11-27 00:52:58 +00003560def hasSheBang(fname):
3561 """Checks fname is a #! script."""
3562 with open(fname) as f:
3563 return f.read(2).startswith('#!')
3564
3565
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003566# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3567def DownloadHooks(*args, **kwargs):
3568 pass
3569
3570
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003571def DownloadGerritHook(force):
3572 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003573
3574 Args:
3575 force: True to update hooks. False to install hooks if not present.
3576 """
3577 if not settings.GetIsGerrit():
3578 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003579 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003580 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3581 if not os.access(dst, os.X_OK):
3582 if os.path.exists(dst):
3583 if not force:
3584 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003585 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003586 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003587 if not hasSheBang(dst):
3588 DieWithError('Not a script: %s\n'
3589 'You need to download from\n%s\n'
3590 'into .git/hooks/commit-msg and '
3591 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003592 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3593 except Exception:
3594 if os.path.exists(dst):
3595 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003596 DieWithError('\nFailed to download hooks.\n'
3597 'You need to download from\n%s\n'
3598 'into .git/hooks/commit-msg and '
3599 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003600
3601
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003602def GetRietveldCodereviewSettingsInteractively():
3603 """Prompt the user for settings."""
3604 server = settings.GetDefaultServerUrl(error_ok=True)
3605 prompt = 'Rietveld server (host[:port])'
3606 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3607 newserver = ask_for_data(prompt + ':')
3608 if not server and not newserver:
3609 newserver = DEFAULT_SERVER
3610 if newserver:
3611 newserver = gclient_utils.UpgradeToHttps(newserver)
3612 if newserver != server:
3613 RunGit(['config', 'rietveld.server', newserver])
3614
3615 def SetProperty(initial, caption, name, is_url):
3616 prompt = caption
3617 if initial:
3618 prompt += ' ("x" to clear) [%s]' % initial
3619 new_val = ask_for_data(prompt + ':')
3620 if new_val == 'x':
3621 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3622 elif new_val:
3623 if is_url:
3624 new_val = gclient_utils.UpgradeToHttps(new_val)
3625 if new_val != initial:
3626 RunGit(['config', 'rietveld.' + name, new_val])
3627
3628 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3629 SetProperty(settings.GetDefaultPrivateFlag(),
3630 'Private flag (rietveld only)', 'private', False)
3631 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3632 'tree-status-url', False)
3633 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3634 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3635 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3636 'run-post-upload-hook', False)
3637
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003638
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003639class _GitCookiesChecker(object):
3640 """Provides facilties for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003641
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003642 _GOOGLESOURCE = 'googlesource.com'
3643
3644 def __init__(self):
3645 # Cached list of [host, identity, source], where source is either
3646 # .gitcookies or .netrc.
3647 self._all_hosts = None
3648
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003649 def ensure_configured_gitcookies(self):
3650 """Runs checks and suggests fixes to make git use .gitcookies from default
3651 path."""
3652 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3653 configured_path = RunGitSilent(
3654 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003655 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003656 if configured_path:
3657 self._ensure_default_gitcookies_path(configured_path, default)
3658 else:
3659 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003660
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003661 @staticmethod
3662 def _ensure_default_gitcookies_path(configured_path, default_path):
3663 assert configured_path
3664 if configured_path == default_path:
3665 print('git is already configured to use your .gitcookies from %s' %
3666 configured_path)
3667 return
3668
3669 print('WARNING: you have configured custom path to .gitcookies: %s\n'
3670 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3671 (configured_path, default_path))
3672
3673 if not os.path.exists(configured_path):
3674 print('However, your configured .gitcookies file is missing.')
3675 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3676 action='reconfigure')
3677 RunGit(['config', '--global', 'http.cookiefile', default_path])
3678 return
3679
3680 if os.path.exists(default_path):
3681 print('WARNING: default .gitcookies file already exists %s' %
3682 default_path)
3683 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3684 default_path)
3685
3686 confirm_or_exit('Move existing .gitcookies to default location?',
3687 action='move')
3688 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003689 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003690 print('Moved and reconfigured git to use .gitcookies from %s' %
3691 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003692
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003693 @staticmethod
3694 def _configure_gitcookies_path(default_path):
3695 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3696 if os.path.exists(netrc_path):
3697 print('You seem to be using outdated .netrc for git credentials: %s' %
3698 netrc_path)
3699 print('This tool will guide you through setting up recommended '
3700 '.gitcookies store for git credentials.\n'
3701 '\n'
3702 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3703 ' git config --global --unset http.cookiefile\n'
3704 ' mv %s %s.backup\n\n' % (default_path, default_path))
3705 confirm_or_exit(action='setup .gitcookies')
3706 RunGit(['config', '--global', 'http.cookiefile', default_path])
3707 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003708
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003709 def get_hosts_with_creds(self, include_netrc=False):
3710 if self._all_hosts is None:
3711 a = gerrit_util.CookiesAuthenticator()
3712 self._all_hosts = [
3713 (h, u, s)
3714 for h, u, s in itertools.chain(
3715 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3716 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3717 )
3718 if h.endswith(self._GOOGLESOURCE)
3719 ]
3720
3721 if include_netrc:
3722 return self._all_hosts
3723 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3724
3725 def print_current_creds(self, include_netrc=False):
3726 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3727 if not hosts:
3728 print('No Git/Gerrit credentials found')
3729 return
3730 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3731 header = [('Host', 'User', 'Which file'),
3732 ['=' * l for l in lengths]]
3733 for row in (header + hosts):
3734 print('\t'.join((('%%+%ds' % l) % s)
3735 for l, s in zip(lengths, row)))
3736
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003737 @staticmethod
3738 def _parse_identity(identity):
3739 """Parses identity "git-<ldap>.example.com" into <ldap> and domain."""
3740 username, domain = identity.split('.', 1)
3741 if username.startswith('git-'):
3742 username = username[len('git-'):]
3743 return username, domain
3744
3745 def _get_usernames_of_domain(self, domain):
3746 """Returns list of usernames referenced by .gitcookies in a given domain."""
3747 identities_by_domain = {}
3748 for _, identity, _ in self.get_hosts_with_creds():
3749 username, domain = self._parse_identity(identity)
3750 identities_by_domain.setdefault(domain, []).append(username)
3751 return identities_by_domain.get(domain)
3752
3753 def _canonical_git_googlesource_host(self, host):
3754 """Normalizes Gerrit hosts (with '-review') to Git host."""
3755 assert host.endswith(self._GOOGLESOURCE)
3756 # Prefix doesn't include '.' at the end.
3757 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3758 if prefix.endswith('-review'):
3759 prefix = prefix[:-len('-review')]
3760 return prefix + '.' + self._GOOGLESOURCE
3761
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003762 def _canonical_gerrit_googlesource_host(self, host):
3763 git_host = self._canonical_git_googlesource_host(host)
3764 prefix = git_host.split('.', 1)[0]
3765 return prefix + '-review.' + self._GOOGLESOURCE
3766
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003767 def has_generic_host(self):
3768 """Returns whether generic .googlesource.com has been configured.
3769
3770 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3771 """
3772 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3773 if host == '.' + self._GOOGLESOURCE:
3774 return True
3775 return False
3776
3777 def _get_git_gerrit_identity_pairs(self):
3778 """Returns map from canonic host to pair of identities (Git, Gerrit).
3779
3780 One of identities might be None, meaning not configured.
3781 """
3782 host_to_identity_pairs = {}
3783 for host, identity, _ in self.get_hosts_with_creds():
3784 canonical = self._canonical_git_googlesource_host(host)
3785 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3786 idx = 0 if canonical == host else 1
3787 pair[idx] = identity
3788 return host_to_identity_pairs
3789
3790 def get_partially_configured_hosts(self):
3791 return set(
3792 host for host, identities_pair in
3793 self._get_git_gerrit_identity_pairs().iteritems()
3794 if None in identities_pair and host != '.' + self._GOOGLESOURCE)
3795
3796 def get_conflicting_hosts(self):
3797 return set(
3798 host for host, (i1, i2) in
3799 self._get_git_gerrit_identity_pairs().iteritems()
3800 if None not in (i1, i2) and i1 != i2)
3801
3802 def get_duplicated_hosts(self):
3803 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3804 return set(host for host, count in counters.iteritems() if count > 1)
3805
3806 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3807 'chromium.googlesource.com': 'chromium.org',
3808 'chrome-internal.googlesource.com': 'google.com',
3809 }
3810
3811 def get_hosts_with_wrong_identities(self):
3812 """Finds hosts which **likely** reference wrong identities.
3813
3814 Note: skips hosts which have conflicting identities for Git and Gerrit.
3815 """
3816 hosts = set()
3817 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3818 pair = self._get_git_gerrit_identity_pairs().get(host)
3819 if pair and pair[0] == pair[1]:
3820 _, domain = self._parse_identity(pair[0])
3821 if domain != expected:
3822 hosts.add(host)
3823 return hosts
3824
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003825 @staticmethod
3826 def print_hosts(hosts, extra_column_func=None):
3827 hosts = sorted(hosts)
3828 assert hosts
3829 if extra_column_func is None:
3830 extras = [''] * len(hosts)
3831 else:
3832 extras = [extra_column_func(host) for host in hosts]
3833 tmpl = ' %%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3834 for he in zip(hosts, extras):
3835 print(tmpl % he)
3836 print()
3837
3838 def find_and_report_problems(self):
3839 """Returns True if there was at least one problem, else False."""
3840 problems = [False]
3841 def add_problem():
3842 if not problems[0]:
Andrii Shyshkalov4812e612017-03-27 17:22:57 +02003843 print('\n\n.gitcookies problem report:\n')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003844 problems[0] = True
3845
3846 if self.has_generic_host():
3847 add_problem()
3848 print(' .googlesource.com record detected\n'
3849 ' Chrome Infrastructure team recommends to list full host names '
3850 'explicitly.\n')
3851
3852 dups = self.get_duplicated_hosts()
3853 if dups:
3854 add_problem()
3855 print(' The following hosts were defined twice:\n')
3856 self.print_hosts(dups)
3857
3858 partial = self.get_partially_configured_hosts()
3859 if partial:
3860 add_problem()
3861 print(' Credentials should come in pairs for Git and Gerrit hosts. '
3862 'These hosts are missing:')
3863 self.print_hosts(partial)
3864
3865 conflicting = self.get_conflicting_hosts()
3866 if conflicting:
3867 add_problem()
3868 print(' The following Git hosts have differing credentials from their '
3869 'Gerrit counterparts:\n')
3870 self.print_hosts(conflicting, lambda host: '%s vs %s' %
3871 tuple(self._get_git_gerrit_identity_pairs()[host]))
3872
3873 wrong = self.get_hosts_with_wrong_identities()
3874 if wrong:
3875 add_problem()
3876 print(' These hosts likely use wrong identity:\n')
3877 self.print_hosts(wrong, lambda host: '%s but %s recommended' %
3878 (self._get_git_gerrit_identity_pairs()[host][0],
3879 self._EXPECTED_HOST_IDENTITY_DOMAINS[host]))
3880 return problems[0]
3881
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003882
3883def CMDcreds_check(parser, args):
3884 """Checks credentials and suggests changes."""
3885 _, _ = parser.parse_args(args)
3886
3887 if gerrit_util.GceAuthenticator.is_gce():
3888 DieWithError('this command is not designed for GCE, are you on a bot?')
3889
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003890 checker = _GitCookiesChecker()
3891 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003892
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003893 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003894 checker.print_current_creds(include_netrc=True)
3895
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003896 if not checker.find_and_report_problems():
3897 print('\nNo problems detected in your .gitcookies')
3898 return 0
3899 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003900
3901
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003902@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003903def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003904 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003905
tandrii5d0a0422016-09-14 06:24:35 -07003906 print('WARNING: git cl config works for Rietveld only')
3907 # TODO(tandrii): remove this once we switch to Gerrit.
3908 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003909 parser.add_option('--activate-update', action='store_true',
3910 help='activate auto-updating [rietveld] section in '
3911 '.git/config')
3912 parser.add_option('--deactivate-update', action='store_true',
3913 help='deactivate auto-updating [rietveld] section in '
3914 '.git/config')
3915 options, args = parser.parse_args(args)
3916
3917 if options.deactivate_update:
3918 RunGit(['config', 'rietveld.autoupdate', 'false'])
3919 return
3920
3921 if options.activate_update:
3922 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3923 return
3924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003925 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003926 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003927 return 0
3928
3929 url = args[0]
3930 if not url.endswith('codereview.settings'):
3931 url = os.path.join(url, 'codereview.settings')
3932
3933 # Load code review settings and download hooks (if available).
3934 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3935 return 0
3936
3937
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003938def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003939 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003940 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3941 branch = ShortBranchName(branchref)
3942 _, args = parser.parse_args(args)
3943 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003944 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003945 return RunGit(['config', 'branch.%s.base-url' % branch],
3946 error_ok=False).strip()
3947 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003948 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003949 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3950 error_ok=False).strip()
3951
3952
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003953def color_for_status(status):
3954 """Maps a Changelist status to color, for CMDstatus and other tools."""
3955 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003956 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003957 'waiting': Fore.BLUE,
3958 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003959 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003960 'lgtm': Fore.GREEN,
3961 'commit': Fore.MAGENTA,
3962 'closed': Fore.CYAN,
3963 'error': Fore.WHITE,
3964 }.get(status, Fore.WHITE)
3965
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003966
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003967def get_cl_statuses(changes, fine_grained, max_processes=None):
3968 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003969
3970 If fine_grained is true, this will fetch CL statuses from the server.
3971 Otherwise, simply indicate if there's a matching url for the given branches.
3972
3973 If max_processes is specified, it is used as the maximum number of processes
3974 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3975 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003976
3977 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003978 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003979 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003980 upload.verbosity = 0
3981
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003982 if not changes:
3983 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003984
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003985 if not fine_grained:
3986 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003987 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003988 for cl in changes:
3989 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003990 return
3991
3992 # First, sort out authentication issues.
3993 logging.debug('ensuring credentials exist')
3994 for cl in changes:
3995 cl.EnsureAuthenticated(force=False, refresh=True)
3996
3997 def fetch(cl):
3998 try:
3999 return (cl, cl.GetStatus())
4000 except:
4001 # See http://crbug.com/629863.
4002 logging.exception('failed to fetch status for %s:', cl)
4003 raise
4004
4005 threads_count = len(changes)
4006 if max_processes:
4007 threads_count = max(1, min(threads_count, max_processes))
4008 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4009
4010 pool = ThreadPool(threads_count)
4011 fetched_cls = set()
4012 try:
4013 it = pool.imap_unordered(fetch, changes).__iter__()
4014 while True:
4015 try:
4016 cl, status = it.next(timeout=5)
4017 except multiprocessing.TimeoutError:
4018 break
4019 fetched_cls.add(cl)
4020 yield cl, status
4021 finally:
4022 pool.close()
4023
4024 # Add any branches that failed to fetch.
4025 for cl in set(changes) - fetched_cls:
4026 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004027
rmistry@google.com2dd99862015-06-22 12:22:18 +00004028
4029def upload_branch_deps(cl, args):
4030 """Uploads CLs of local branches that are dependents of the current branch.
4031
4032 If the local branch dependency tree looks like:
4033 test1 -> test2.1 -> test3.1
4034 -> test3.2
4035 -> test2.2 -> test3.3
4036
4037 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4038 run on the dependent branches in this order:
4039 test2.1, test3.1, test3.2, test2.2, test3.3
4040
4041 Note: This function does not rebase your local dependent branches. Use it when
4042 you make a change to the parent branch that will not conflict with its
4043 dependent branches, and you would like their dependencies updated in
4044 Rietveld.
4045 """
4046 if git_common.is_dirty_git_tree('upload-branch-deps'):
4047 return 1
4048
4049 root_branch = cl.GetBranch()
4050 if root_branch is None:
4051 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4052 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004053 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004054 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4055 'patchset dependencies without an uploaded CL.')
4056
4057 branches = RunGit(['for-each-ref',
4058 '--format=%(refname:short) %(upstream:short)',
4059 'refs/heads'])
4060 if not branches:
4061 print('No local branches found.')
4062 return 0
4063
4064 # Create a dictionary of all local branches to the branches that are dependent
4065 # on it.
4066 tracked_to_dependents = collections.defaultdict(list)
4067 for b in branches.splitlines():
4068 tokens = b.split()
4069 if len(tokens) == 2:
4070 branch_name, tracked = tokens
4071 tracked_to_dependents[tracked].append(branch_name)
4072
vapiera7fbd5a2016-06-16 09:17:49 -07004073 print()
4074 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004075 dependents = []
4076 def traverse_dependents_preorder(branch, padding=''):
4077 dependents_to_process = tracked_to_dependents.get(branch, [])
4078 padding += ' '
4079 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004080 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004081 dependents.append(dependent)
4082 traverse_dependents_preorder(dependent, padding)
4083 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004084 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004085
4086 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004087 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004088 return 0
4089
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004090 confirm_or_exit('This command will checkout all dependent branches and run '
4091 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004092
andybons@chromium.org962f9462016-02-03 20:00:42 +00004093 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004094 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004095 args.extend(['-t', 'Updated patchset dependency'])
4096
rmistry@google.com2dd99862015-06-22 12:22:18 +00004097 # Record all dependents that failed to upload.
4098 failures = {}
4099 # Go through all dependents, checkout the branch and upload.
4100 try:
4101 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004102 print()
4103 print('--------------------------------------')
4104 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004105 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004106 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004107 try:
4108 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004109 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004110 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004111 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004112 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004113 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004114 finally:
4115 # Swap back to the original root branch.
4116 RunGit(['checkout', '-q', root_branch])
4117
vapiera7fbd5a2016-06-16 09:17:49 -07004118 print()
4119 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004120 for dependent_branch in dependents:
4121 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004122 print(' %s : %s' % (dependent_branch, upload_status))
4123 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004124
4125 return 0
4126
4127
kmarshall3bff56b2016-06-06 18:31:47 -07004128def CMDarchive(parser, args):
4129 """Archives and deletes branches associated with closed changelists."""
4130 parser.add_option(
4131 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004132 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004133 parser.add_option(
4134 '-f', '--force', action='store_true',
4135 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004136 parser.add_option(
4137 '-d', '--dry-run', action='store_true',
4138 help='Skip the branch tagging and removal steps.')
4139 parser.add_option(
4140 '-t', '--notags', action='store_true',
4141 help='Do not tag archived branches. '
4142 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004143
4144 auth.add_auth_options(parser)
4145 options, args = parser.parse_args(args)
4146 if args:
4147 parser.error('Unsupported args: %s' % ' '.join(args))
4148 auth_config = auth.extract_auth_config_from_options(options)
4149
4150 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4151 if not branches:
4152 return 0
4153
vapiera7fbd5a2016-06-16 09:17:49 -07004154 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004155 changes = [Changelist(branchref=b, auth_config=auth_config)
4156 for b in branches.splitlines()]
4157 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4158 statuses = get_cl_statuses(changes,
4159 fine_grained=True,
4160 max_processes=options.maxjobs)
4161 proposal = [(cl.GetBranch(),
4162 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4163 for cl, status in statuses
4164 if status == 'closed']
4165 proposal.sort()
4166
4167 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004168 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004169 return 0
4170
4171 current_branch = GetCurrentBranch()
4172
vapiera7fbd5a2016-06-16 09:17:49 -07004173 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004174 if options.notags:
4175 for next_item in proposal:
4176 print(' ' + next_item[0])
4177 else:
4178 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4179 for next_item in proposal:
4180 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004181
kmarshall9249e012016-08-23 12:02:16 -07004182 # Quit now on precondition failure or if instructed by the user, either
4183 # via an interactive prompt or by command line flags.
4184 if options.dry_run:
4185 print('\nNo changes were made (dry run).\n')
4186 return 0
4187 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004188 print('You are currently on a branch \'%s\' which is associated with a '
4189 'closed codereview issue, so archive cannot proceed. Please '
4190 'checkout another branch and run this command again.' %
4191 current_branch)
4192 return 1
kmarshall9249e012016-08-23 12:02:16 -07004193 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004194 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4195 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004196 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004197 return 1
4198
4199 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004200 if not options.notags:
4201 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004202 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004203
vapiera7fbd5a2016-06-16 09:17:49 -07004204 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004205
4206 return 0
4207
4208
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004209def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004210 """Show status of changelists.
4211
4212 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004213 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004214 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004215 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004216 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004217 - Magenta in the commit queue
4218 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004219 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004220
4221 Also see 'git cl comments'.
4222 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004223 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004224 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004225 parser.add_option('-f', '--fast', action='store_true',
4226 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004227 parser.add_option(
4228 '-j', '--maxjobs', action='store', type=int,
4229 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004230
4231 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004232 _add_codereview_issue_select_options(
4233 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004234 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004235 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004236 if args:
4237 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004238 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004239
iannuccie53c9352016-08-17 14:40:40 -07004240 if options.issue is not None and not options.field:
4241 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004243 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004244 cl = Changelist(auth_config=auth_config, issue=options.issue,
4245 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004246 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004247 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004248 elif options.field == 'id':
4249 issueid = cl.GetIssue()
4250 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004251 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004252 elif options.field == 'patch':
4253 patchset = cl.GetPatchset()
4254 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004255 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004256 elif options.field == 'status':
4257 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004258 elif options.field == 'url':
4259 url = cl.GetIssueURL()
4260 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004261 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004262 return 0
4263
4264 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4265 if not branches:
4266 print('No local branch found.')
4267 return 0
4268
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004269 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004270 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004271 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004272 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004273 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004274 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004275 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004276
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004277 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004278 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4279 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4280 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004281 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004282 c, status = output.next()
4283 branch_statuses[c.GetBranch()] = status
4284 status = branch_statuses.pop(branch)
4285 url = cl.GetIssueURL()
4286 if url and (not status or status == 'error'):
4287 # The issue probably doesn't exist anymore.
4288 url += ' (broken)'
4289
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004290 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004291 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004292 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004293 color = ''
4294 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004295 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004296 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004297 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004298 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004299
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004300
4301 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004302 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004303 print('Current branch: %s' % branch)
4304 for cl in changes:
4305 if cl.GetBranch() == branch:
4306 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004307 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004308 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004309 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004310 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004311 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004312 print('Issue description:')
4313 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004314 return 0
4315
4316
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004317def colorize_CMDstatus_doc():
4318 """To be called once in main() to add colors to git cl status help."""
4319 colors = [i for i in dir(Fore) if i[0].isupper()]
4320
4321 def colorize_line(line):
4322 for color in colors:
4323 if color in line.upper():
4324 # Extract whitespaces first and the leading '-'.
4325 indent = len(line) - len(line.lstrip(' ')) + 1
4326 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4327 return line
4328
4329 lines = CMDstatus.__doc__.splitlines()
4330 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4331
4332
phajdan.jre328cf92016-08-22 04:12:17 -07004333def write_json(path, contents):
4334 with open(path, 'w') as f:
4335 json.dump(contents, f)
4336
4337
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004338@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004339def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004340 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004341
4342 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004343 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004344 parser.add_option('-r', '--reverse', action='store_true',
4345 help='Lookup the branch(es) for the specified issues. If '
4346 'no issues are specified, all branches with mapped '
4347 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07004348 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004349 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004350 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004351 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004352
dnj@chromium.org406c4402015-03-03 17:22:28 +00004353 if options.reverse:
4354 branches = RunGit(['for-each-ref', 'refs/heads',
4355 '--format=%(refname:short)']).splitlines()
4356
4357 # Reverse issue lookup.
4358 issue_branch_map = {}
4359 for branch in branches:
4360 cl = Changelist(branchref=branch)
4361 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4362 if not args:
4363 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004364 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004365 for issue in args:
4366 if not issue:
4367 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004368 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004369 print('Branch for issue number %s: %s' % (
4370 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004371 if options.json:
4372 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004373 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004374 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004375 if len(args) > 0:
4376 try:
4377 issue = int(args[0])
4378 except ValueError:
4379 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00004380 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00004381 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07004382 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07004383 if options.json:
4384 write_json(options.json, {
4385 'issue': cl.GetIssue(),
4386 'issue_url': cl.GetIssueURL(),
4387 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004388 return 0
4389
4390
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004391def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004392 """Shows or posts review comments for any changelist."""
4393 parser.add_option('-a', '--add-comment', dest='comment',
4394 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004395 parser.add_option('-i', '--issue', dest='issue',
4396 help='review issue id (defaults to current issue). '
4397 'If given, requires --rietveld or --gerrit')
smut@google.comc85ac942015-09-15 16:34:43 +00004398 parser.add_option('-j', '--json-file',
4399 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004400 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004401 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004402 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004403 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004404 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004405
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004406 issue = None
4407 if options.issue:
4408 try:
4409 issue = int(options.issue)
4410 except ValueError:
4411 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004412 if not options.forced_codereview:
4413 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004414
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004415 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004416 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004417 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004418
4419 if options.comment:
4420 cl.AddComment(options.comment)
4421 return 0
4422
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004423 summary = sorted(cl.GetCommentsSummary(), key=lambda c: c.date)
4424 for comment in summary:
4425 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004426 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004427 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004428 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004429 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004430 color = Fore.MAGENTA
4431 else:
4432 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004433 print('\n%s%s %s%s\n%s' % (
4434 color,
4435 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4436 comment.sender,
4437 Fore.RESET,
4438 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4439
smut@google.comc85ac942015-09-15 16:34:43 +00004440 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004441 def pre_serialize(c):
4442 dct = c.__dict__.copy()
4443 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4444 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004445 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004446 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004447 return 0
4448
4449
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004450@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004451def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004452 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004453 parser.add_option('-d', '--display', action='store_true',
4454 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004455 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004456 help='New description to set for this issue (- for stdin, '
4457 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004458 parser.add_option('-f', '--force', action='store_true',
4459 help='Delete any unpublished Gerrit edits for this issue '
4460 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004461
4462 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004463 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004464 options, args = parser.parse_args(args)
4465 _process_codereview_select_options(parser, options)
4466
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004467 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004468 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004469 target_issue_arg = ParseIssueNumberArgument(args[0],
4470 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004471 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004472 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004473
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004474 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004475
martiniss6eda05f2016-06-30 10:18:35 -07004476 kwargs = {
4477 'auth_config': auth_config,
4478 'codereview': options.forced_codereview,
4479 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004480 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004481 if target_issue_arg:
4482 kwargs['issue'] = target_issue_arg.issue
4483 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004484 if target_issue_arg.codereview and not options.forced_codereview:
4485 detected_codereview_from_url = True
4486 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004487
4488 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004489 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004490 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004491 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004492
4493 if detected_codereview_from_url:
4494 logging.info('canonical issue/change URL: %s (type: %s)\n',
4495 cl.GetIssueURL(), target_issue_arg.codereview)
4496
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004497 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004498
smut@google.com34fb6b12015-07-13 20:03:26 +00004499 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004500 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004501 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004502
4503 if options.new_description:
4504 text = options.new_description
4505 if text == '-':
4506 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004507 elif text == '+':
4508 base_branch = cl.GetCommonAncestorWithUpstream()
4509 change = cl.GetChange(base_branch, None, local_description=True)
4510 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004511
4512 description.set_description(text)
4513 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004514 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004515
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004516 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004517 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004518 return 0
4519
4520
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004521def CreateDescriptionFromLog(args):
4522 """Pulls out the commit log to use as a base for the CL description."""
4523 log_args = []
4524 if len(args) == 1 and not args[0].endswith('.'):
4525 log_args = [args[0] + '..']
4526 elif len(args) == 1 and args[0].endswith('...'):
4527 log_args = [args[0][:-1]]
4528 elif len(args) == 2:
4529 log_args = [args[0] + '..' + args[1]]
4530 else:
4531 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004532 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004533
4534
thestig@chromium.org44202a22014-03-11 19:22:18 +00004535def CMDlint(parser, args):
4536 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004537 parser.add_option('--filter', action='append', metavar='-x,+y',
4538 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004539 auth.add_auth_options(parser)
4540 options, args = parser.parse_args(args)
4541 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004542
4543 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004544 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004545 try:
4546 import cpplint
4547 import cpplint_chromium
4548 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004549 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004550 return 1
4551
4552 # Change the current working directory before calling lint so that it
4553 # shows the correct base.
4554 previous_cwd = os.getcwd()
4555 os.chdir(settings.GetRoot())
4556 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004557 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004558 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4559 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004560 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004561 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004562 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004563
4564 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004565 command = args + files
4566 if options.filter:
4567 command = ['--filter=' + ','.join(options.filter)] + command
4568 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004569
4570 white_regex = re.compile(settings.GetLintRegex())
4571 black_regex = re.compile(settings.GetLintIgnoreRegex())
4572 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4573 for filename in filenames:
4574 if white_regex.match(filename):
4575 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004576 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004577 else:
4578 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4579 extra_check_functions)
4580 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004581 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004582 finally:
4583 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004584 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004585 if cpplint._cpplint_state.error_count != 0:
4586 return 1
4587 return 0
4588
4589
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004590def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004591 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004592 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004593 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004594 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004595 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004596 auth.add_auth_options(parser)
4597 options, args = parser.parse_args(args)
4598 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004599
sbc@chromium.org71437c02015-04-09 19:29:40 +00004600 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004601 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004602 return 1
4603
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004604 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004605 if args:
4606 base_branch = args[0]
4607 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004608 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004609 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004610
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004611 cl.RunHook(
4612 committing=not options.upload,
4613 may_prompt=False,
4614 verbose=options.verbose,
4615 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004616 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004617
4618
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004619def GenerateGerritChangeId(message):
4620 """Returns Ixxxxxx...xxx change id.
4621
4622 Works the same way as
4623 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4624 but can be called on demand on all platforms.
4625
4626 The basic idea is to generate git hash of a state of the tree, original commit
4627 message, author/committer info and timestamps.
4628 """
4629 lines = []
4630 tree_hash = RunGitSilent(['write-tree'])
4631 lines.append('tree %s' % tree_hash.strip())
4632 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4633 if code == 0:
4634 lines.append('parent %s' % parent.strip())
4635 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4636 lines.append('author %s' % author.strip())
4637 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4638 lines.append('committer %s' % committer.strip())
4639 lines.append('')
4640 # Note: Gerrit's commit-hook actually cleans message of some lines and
4641 # whitespace. This code is not doing this, but it clearly won't decrease
4642 # entropy.
4643 lines.append(message)
4644 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4645 stdin='\n'.join(lines))
4646 return 'I%s' % change_hash.strip()
4647
4648
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004649def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004650 """Computes the remote branch ref to use for the CL.
4651
4652 Args:
4653 remote (str): The git remote for the CL.
4654 remote_branch (str): The git remote branch for the CL.
4655 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004656 """
4657 if not (remote and remote_branch):
4658 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004659
wittman@chromium.org455dc922015-01-26 20:15:50 +00004660 if target_branch:
4661 # Cannonicalize branch references to the equivalent local full symbolic
4662 # refs, which are then translated into the remote full symbolic refs
4663 # below.
4664 if '/' not in target_branch:
4665 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4666 else:
4667 prefix_replacements = (
4668 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4669 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4670 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4671 )
4672 match = None
4673 for regex, replacement in prefix_replacements:
4674 match = re.search(regex, target_branch)
4675 if match:
4676 remote_branch = target_branch.replace(match.group(0), replacement)
4677 break
4678 if not match:
4679 # This is a branch path but not one we recognize; use as-is.
4680 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004681 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4682 # Handle the refs that need to land in different refs.
4683 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004684
wittman@chromium.org455dc922015-01-26 20:15:50 +00004685 # Create the true path to the remote branch.
4686 # Does the following translation:
4687 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4688 # * refs/remotes/origin/master -> refs/heads/master
4689 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4690 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4691 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4692 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4693 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4694 'refs/heads/')
4695 elif remote_branch.startswith('refs/remotes/branch-heads'):
4696 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004697
wittman@chromium.org455dc922015-01-26 20:15:50 +00004698 return remote_branch
4699
4700
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004701def cleanup_list(l):
4702 """Fixes a list so that comma separated items are put as individual items.
4703
4704 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4705 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4706 """
4707 items = sum((i.split(',') for i in l), [])
4708 stripped_items = (i.strip() for i in items)
4709 return sorted(filter(None, stripped_items))
4710
4711
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004712@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004713def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004714 """Uploads the current changelist to codereview.
4715
4716 Can skip dependency patchset uploads for a branch by running:
4717 git config branch.branch_name.skip-deps-uploads True
4718 To unset run:
4719 git config --unset branch.branch_name.skip-deps-uploads
4720 Can also set the above globally by using the --global flag.
4721 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004722 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4723 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004724 parser.add_option('--bypass-watchlists', action='store_true',
4725 dest='bypass_watchlists',
4726 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004727 parser.add_option('-f', action='store_true', dest='force',
4728 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004729 parser.add_option('--message', '-m', dest='message',
4730 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004731 parser.add_option('-b', '--bug',
4732 help='pre-populate the bug number(s) for this issue. '
4733 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004734 parser.add_option('--message-file', dest='message_file',
4735 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004736 parser.add_option('--title', '-t', dest='title',
4737 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004738 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004739 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004740 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004741 parser.add_option('--tbrs',
4742 action='append', default=[],
4743 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004744 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004745 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004746 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004747 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004748 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004749 parser.add_option('--emulate_svn_auto_props',
4750 '--emulate-svn-auto-props',
4751 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004752 dest="emulate_svn_auto_props",
4753 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004754 parser.add_option('-c', '--use-commit-queue', action='store_true',
4755 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004756 parser.add_option('--private', action='store_true',
4757 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004758 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004759 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004760 metavar='TARGET',
4761 help='Apply CL to remote ref TARGET. ' +
4762 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004763 parser.add_option('--squash', action='store_true',
4764 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004765 parser.add_option('--no-squash', action='store_true',
4766 help='Don\'t squash multiple commits into one ' +
4767 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004768 parser.add_option('--topic', default=None,
4769 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004770 parser.add_option('--email', default=None,
4771 help='email address to use to connect to Rietveld')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004772 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4773 const='TBR', help='add a set of OWNERS to TBR')
4774 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4775 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004776 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4777 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004778 help='Send the patchset to do a CQ dry run right after '
4779 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004780 parser.add_option('--dependencies', action='store_true',
4781 help='Uploads CLs of all the local branches that depend on '
4782 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004783
rmistry@google.com2dd99862015-06-22 12:22:18 +00004784 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004785 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004786 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004787 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004788 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004789 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004790 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004791
sbc@chromium.org71437c02015-04-09 19:29:40 +00004792 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004793 return 1
4794
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004795 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004796 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004797 options.cc = cleanup_list(options.cc)
4798
tandriib80458a2016-06-23 12:20:07 -07004799 if options.message_file:
4800 if options.message:
4801 parser.error('only one of --message and --message-file allowed.')
4802 options.message = gclient_utils.FileRead(options.message_file)
4803 options.message_file = None
4804
tandrii4d0545a2016-07-06 03:56:49 -07004805 if options.cq_dry_run and options.use_commit_queue:
4806 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4807
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004808 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4809 settings.GetIsGerrit()
4810
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004811 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004812 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004813
4814
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004815@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004816def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004817 """DEPRECATED: Used to commit the current changelist via git-svn."""
4818 message = ('git-cl no longer supports committing to SVN repositories via '
4819 'git-svn. You probably want to use `git cl land` instead.')
4820 print(message)
4821 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004822
4823
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004824# Two special branches used by git cl land.
4825MERGE_BRANCH = 'git-cl-commit'
4826CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4827
4828
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004829@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004830def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004831 """Commits the current changelist via git.
4832
4833 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4834 upstream and closes the issue automatically and atomically.
4835
4836 Otherwise (in case of Rietveld):
4837 Squashes branch into a single commit.
4838 Updates commit message with metadata (e.g. pointer to review).
4839 Pushes the code upstream.
4840 Updates review and closes.
4841 """
4842 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4843 help='bypass upload presubmit hook')
4844 parser.add_option('-m', dest='message',
4845 help="override review description")
4846 parser.add_option('-f', action='store_true', dest='force',
4847 help="force yes to questions (don't prompt)")
4848 parser.add_option('-c', dest='contributor',
4849 help="external contributor for patch (appended to " +
4850 "description and used as author for git). Should be " +
4851 "formatted as 'First Last <email@example.com>'")
4852 add_git_similarity(parser)
4853 auth.add_auth_options(parser)
4854 (options, args) = parser.parse_args(args)
4855 auth_config = auth.extract_auth_config_from_options(options)
4856
4857 cl = Changelist(auth_config=auth_config)
4858
4859 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4860 if cl.IsGerrit():
4861 if options.message:
4862 # This could be implemented, but it requires sending a new patch to
4863 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4864 # Besides, Gerrit has the ability to change the commit message on submit
4865 # automatically, thus there is no need to support this option (so far?).
4866 parser.error('-m MESSAGE option is not supported for Gerrit.')
4867 if options.contributor:
4868 parser.error(
4869 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4870 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4871 'the contributor\'s "name <email>". If you can\'t upload such a '
4872 'commit for review, contact your repository admin and request'
4873 '"Forge-Author" permission.')
4874 if not cl.GetIssue():
4875 DieWithError('You must upload the change first to Gerrit.\n'
4876 ' If you would rather have `git cl land` upload '
4877 'automatically for you, see http://crbug.com/642759')
4878 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4879 options.verbose)
4880
4881 current = cl.GetBranch()
4882 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4883 if remote == '.':
4884 print()
4885 print('Attempting to push branch %r into another local branch!' % current)
4886 print()
4887 print('Either reparent this branch on top of origin/master:')
4888 print(' git reparent-branch --root')
4889 print()
4890 print('OR run `git rebase-update` if you think the parent branch is ')
4891 print('already committed.')
4892 print()
4893 print(' Current parent: %r' % upstream_branch)
4894 return 1
4895
4896 if not args:
4897 # Default to merging against our best guess of the upstream branch.
4898 args = [cl.GetUpstreamBranch()]
4899
4900 if options.contributor:
4901 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4902 print("Please provide contibutor as 'First Last <email@example.com>'")
4903 return 1
4904
4905 base_branch = args[0]
4906
4907 if git_common.is_dirty_git_tree('land'):
4908 return 1
4909
4910 # This rev-list syntax means "show all commits not in my branch that
4911 # are in base_branch".
4912 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4913 base_branch]).splitlines()
4914 if upstream_commits:
4915 print('Base branch "%s" has %d commits '
4916 'not in this branch.' % (base_branch, len(upstream_commits)))
4917 print('Run "git merge %s" before attempting to land.' % base_branch)
4918 return 1
4919
4920 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4921 if not options.bypass_hooks:
4922 author = None
4923 if options.contributor:
4924 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4925 hook_results = cl.RunHook(
4926 committing=True,
4927 may_prompt=not options.force,
4928 verbose=options.verbose,
4929 change=cl.GetChange(merge_base, author))
4930 if not hook_results.should_continue():
4931 return 1
4932
4933 # Check the tree status if the tree status URL is set.
4934 status = GetTreeStatus()
4935 if 'closed' == status:
4936 print('The tree is closed. Please wait for it to reopen. Use '
4937 '"git cl land --bypass-hooks" to commit on a closed tree.')
4938 return 1
4939 elif 'unknown' == status:
4940 print('Unable to determine tree status. Please verify manually and '
4941 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4942 return 1
4943
4944 change_desc = ChangeDescription(options.message)
4945 if not change_desc.description and cl.GetIssue():
4946 change_desc = ChangeDescription(cl.GetDescription())
4947
4948 if not change_desc.description:
4949 if not cl.GetIssue() and options.bypass_hooks:
4950 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4951 else:
4952 print('No description set.')
4953 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4954 return 1
4955
4956 # Keep a separate copy for the commit message, because the commit message
4957 # contains the link to the Rietveld issue, while the Rietveld message contains
4958 # the commit viewvc url.
4959 if cl.GetIssue():
Aaron Gablea1bab272017-04-11 16:38:18 -07004960 change_desc.update_reviewers(
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004961 get_approving_reviewers(cl.GetIssueProperties()), [])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004962
4963 commit_desc = ChangeDescription(change_desc.description)
4964 if cl.GetIssue():
4965 # Xcode won't linkify this URL unless there is a non-whitespace character
4966 # after it. Add a period on a new line to circumvent this. Also add a space
4967 # before the period to make sure that Gitiles continues to correctly resolve
4968 # the URL.
4969 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4970 if options.contributor:
4971 commit_desc.append_footer('Patch from %s.' % options.contributor)
4972
4973 print('Description:')
4974 print(commit_desc.description)
4975
4976 branches = [merge_base, cl.GetBranchRef()]
4977 if not options.force:
4978 print_stats(options.similarity, options.find_copies, branches)
4979
4980 # We want to squash all this branch's commits into one commit with the proper
4981 # description. We do this by doing a "reset --soft" to the base branch (which
4982 # keeps the working copy the same), then landing that.
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004983 # Delete the special branches if they exist.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004984 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4985 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4986 result = RunGitWithCode(showref_cmd)
4987 if result[0] == 0:
4988 RunGit(['branch', '-D', branch])
4989
4990 # We might be in a directory that's present in this branch but not in the
4991 # trunk. Move up to the top of the tree so that git commands that expect a
4992 # valid CWD won't fail after we check out the merge branch.
4993 rel_base_path = settings.GetRelativeRoot()
4994 if rel_base_path:
4995 os.chdir(rel_base_path)
4996
4997 # Stuff our change into the merge branch.
4998 # We wrap in a try...finally block so if anything goes wrong,
4999 # we clean up the branches.
5000 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005001 revision = None
5002 try:
5003 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
5004 RunGit(['reset', '--soft', merge_base])
5005 if options.contributor:
5006 RunGit(
5007 [
5008 'commit', '--author', options.contributor,
5009 '-m', commit_desc.description,
5010 ])
5011 else:
5012 RunGit(['commit', '-m', commit_desc.description])
5013
5014 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
5015 mirror = settings.GetGitMirror(remote)
5016 if mirror:
5017 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005018 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005019 else:
5020 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005021 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005022 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
5023
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005024 retcode = PushToGitWithAutoRebase(
5025 pushurl, branch, commit_desc.description, git_numberer_enabled)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005026 if retcode == 0:
5027 revision = RunGit(['rev-parse', 'HEAD']).strip()
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005028 if git_numberer_enabled:
5029 change_desc = ChangeDescription(
5030 RunGit(['show', '-s', '--format=%B', 'HEAD']).strip())
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005031 except: # pylint: disable=bare-except
5032 if _IS_BEING_TESTED:
5033 logging.exception('this is likely your ACTUAL cause of test failure.\n'
5034 + '-' * 30 + '8<' + '-' * 30)
5035 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
5036 raise
5037 finally:
5038 # And then swap back to the original branch and clean up.
5039 RunGit(['checkout', '-q', cl.GetBranch()])
5040 RunGit(['branch', '-D', MERGE_BRANCH])
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005041 RunGit(['branch', '-D', CHERRY_PICK_BRANCH], error_ok=True)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005042
5043 if not revision:
5044 print('Failed to push. If this persists, please file a bug.')
5045 return 1
5046
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005047 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005048 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005049 if viewvc_url and revision:
5050 change_desc.append_footer(
5051 'Committed: %s%s' % (viewvc_url, revision))
5052 elif revision:
5053 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005054 print('Closing issue '
5055 '(you may be prompted for your codereview password)...')
5056 cl.UpdateDescription(change_desc.description)
5057 cl.CloseIssue()
5058 props = cl.GetIssueProperties()
5059 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005060 comment = "Committed patchset #%d (id:%d) manually as %s" % (
5061 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005062 if options.bypass_hooks:
5063 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
5064 else:
5065 comment += ' (presubmit successful).'
5066 cl.RpcServer().add_comment(cl.GetIssue(), comment)
5067
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005068 if os.path.isfile(POSTUPSTREAM_HOOK):
5069 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
5070
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005071 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005072
5073
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005074def PushToGitWithAutoRebase(remote, branch, original_description,
5075 git_numberer_enabled, max_attempts=3):
5076 """Pushes current HEAD commit on top of remote's branch.
5077
5078 Attempts to fetch and autorebase on push failures.
5079 Adds git number footers on the fly.
5080
5081 Returns integer code from last command.
5082 """
5083 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5084 code = 0
5085 attempts_left = max_attempts
5086 while attempts_left:
5087 attempts_left -= 1
5088 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5089
5090 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5091 # If fetch fails, retry.
5092 print('Fetching %s/%s...' % (remote, branch))
5093 code, out = RunGitWithCode(
5094 ['retry', 'fetch', remote,
5095 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5096 if code:
5097 print('Fetch failed with exit code %d.' % code)
5098 print(out.strip())
5099 continue
5100
5101 print('Cherry-picking commit on top of latest %s' % branch)
5102 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5103 suppress_stderr=True)
5104 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5105 code, out = RunGitWithCode(['cherry-pick', cherry])
5106 if code:
5107 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5108 'the following files have merge conflicts:' %
5109 (branch, parent_hash))
5110 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
5111 print('Please rebase your patch and try again.')
5112 RunGitWithCode(['cherry-pick', '--abort'])
5113 break
5114
5115 commit_desc = ChangeDescription(original_description)
5116 if git_numberer_enabled:
5117 logging.debug('Adding git number footers')
5118 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5119 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5120 branch)
5121 # Ensure timestamps are monotonically increasing.
5122 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5123 _get_committer_timestamp('HEAD'))
5124 _git_amend_head(commit_desc.description, timestamp)
5125
5126 code, out = RunGitWithCode(
5127 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5128 print(out)
5129 if code == 0:
5130 break
5131 if IsFatalPushFailure(out):
5132 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005133 'user.email are correct and you have push access to the repo.\n'
5134 'Hint: run command below to diangose common Git/Gerrit credential '
5135 'problems:\n'
5136 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005137 break
5138 return code
5139
5140
5141def IsFatalPushFailure(push_stdout):
5142 """True if retrying push won't help."""
5143 return '(prohibited by Gerrit)' in push_stdout
5144
5145
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005146@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005147def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005148 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005149 parser.add_option('-b', dest='newbranch',
5150 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005151 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005152 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005153 parser.add_option('-d', '--directory', action='store', metavar='DIR',
5154 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005155 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005156 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005157 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005158 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005159 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005160 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005161
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005162
5163 group = optparse.OptionGroup(
5164 parser,
5165 'Options for continuing work on the current issue uploaded from a '
5166 'different clone (e.g. different machine). Must be used independently '
5167 'from the other options. No issue number should be specified, and the '
5168 'branch must have an issue number associated with it')
5169 group.add_option('--reapply', action='store_true', dest='reapply',
5170 help='Reset the branch and reapply the issue.\n'
5171 'CAUTION: This will undo any local changes in this '
5172 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005173
5174 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005175 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005176 parser.add_option_group(group)
5177
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005178 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005179 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005180 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005181 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005182 auth_config = auth.extract_auth_config_from_options(options)
5183
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005184 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005185 if options.newbranch:
5186 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005187 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005188 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005189
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005190 cl = Changelist(auth_config=auth_config,
5191 codereview=options.forced_codereview)
5192 if not cl.GetIssue():
5193 parser.error('current branch must have an associated issue')
5194
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005195 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005196 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005197 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005198
5199 RunGit(['reset', '--hard', upstream])
5200 if options.pull:
5201 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005202
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005203 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5204 options.directory)
5205
5206 if len(args) != 1 or not args[0]:
5207 parser.error('Must specify issue number or url')
5208
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005209 target_issue_arg = ParseIssueNumberArgument(args[0],
5210 options.forced_codereview)
5211 if not target_issue_arg.valid:
5212 parser.error('invalid codereview url or CL id')
5213
5214 cl_kwargs = {
5215 'auth_config': auth_config,
5216 'codereview_host': target_issue_arg.hostname,
5217 'codereview': options.forced_codereview,
5218 }
5219 detected_codereview_from_url = False
5220 if target_issue_arg.codereview and not options.forced_codereview:
5221 detected_codereview_from_url = True
5222 cl_kwargs['codereview'] = target_issue_arg.codereview
5223 cl_kwargs['issue'] = target_issue_arg.issue
5224
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005225 # We don't want uncommitted changes mixed up with the patch.
5226 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005227 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005228
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005229 if options.newbranch:
5230 if options.force:
5231 RunGit(['branch', '-D', options.newbranch],
5232 stderr=subprocess2.PIPE, error_ok=True)
5233 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07005234 elif not GetCurrentBranch():
5235 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005236
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005237 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005238
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005239 if cl.IsGerrit():
5240 if options.reject:
5241 parser.error('--reject is not supported with Gerrit codereview.')
5242 if options.nocommit:
5243 parser.error('--nocommit is not supported with Gerrit codereview.')
5244 if options.directory:
5245 parser.error('--directory is not supported with Gerrit codereview.')
5246
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005247 if detected_codereview_from_url:
5248 print('canonical issue/change URL: %s (type: %s)\n' %
5249 (cl.GetIssueURL(), target_issue_arg.codereview))
5250
5251 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
5252 options.nocommit, options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005253
5254
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005255def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005256 """Fetches the tree status and returns either 'open', 'closed',
5257 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005258 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005259 if url:
5260 status = urllib2.urlopen(url).read().lower()
5261 if status.find('closed') != -1 or status == '0':
5262 return 'closed'
5263 elif status.find('open') != -1 or status == '1':
5264 return 'open'
5265 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005266 return 'unset'
5267
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005268
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005269def GetTreeStatusReason():
5270 """Fetches the tree status from a json url and returns the message
5271 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005272 url = settings.GetTreeStatusUrl()
5273 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005274 connection = urllib2.urlopen(json_url)
5275 status = json.loads(connection.read())
5276 connection.close()
5277 return status['message']
5278
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005279
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005280def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005281 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005282 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005283 status = GetTreeStatus()
5284 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005285 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005286 return 2
5287
vapiera7fbd5a2016-06-16 09:17:49 -07005288 print('The tree is %s' % status)
5289 print()
5290 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005291 if status != 'open':
5292 return 1
5293 return 0
5294
5295
maruel@chromium.org15192402012-09-06 12:38:29 +00005296def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005297 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005298 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005299 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005300 '-b', '--bot', action='append',
5301 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5302 'times to specify multiple builders. ex: '
5303 '"-b win_rel -b win_layout". See '
5304 'the try server waterfall for the builders name and the tests '
5305 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005306 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005307 '-B', '--bucket', default='',
5308 help=('Buildbucket bucket to send the try requests.'))
5309 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005310 '-m', '--master', default='',
5311 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005312 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005313 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005314 help='Revision to use for the try job; default: the revision will '
5315 'be determined by the try recipe that builder runs, which usually '
5316 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005317 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005318 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005319 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005320 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005321 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005322 '--project',
5323 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005324 'in recipe to determine to which repository or directory to '
5325 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005326 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005327 '-p', '--property', dest='properties', action='append', default=[],
5328 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005329 'key2=value2 etc. The value will be treated as '
5330 'json if decodable, or as string otherwise. '
5331 'NOTE: using this may make your try job not usable for CQ, '
5332 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005333 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005334 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5335 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005336 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005337 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005338 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005339 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005340
machenbach@chromium.org45453142015-09-15 08:45:22 +00005341 # Make sure that all properties are prop=value pairs.
5342 bad_params = [x for x in options.properties if '=' not in x]
5343 if bad_params:
5344 parser.error('Got properties with missing "=": %s' % bad_params)
5345
maruel@chromium.org15192402012-09-06 12:38:29 +00005346 if args:
5347 parser.error('Unknown arguments: %s' % args)
5348
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005349 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00005350 if not cl.GetIssue():
5351 parser.error('Need to upload first')
5352
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005353 if cl.IsGerrit():
5354 # HACK: warm up Gerrit change detail cache to save on RPCs.
5355 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5356
tandriie113dfd2016-10-11 10:20:12 -07005357 error_message = cl.CannotTriggerTryJobReason()
5358 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005359 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005360
borenet6c0efe62016-10-19 08:13:29 -07005361 if options.bucket and options.master:
5362 parser.error('Only one of --bucket and --master may be used.')
5363
qyearsley1fdfcb62016-10-24 13:22:03 -07005364 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005365
qyearsleydd49f942016-10-28 11:57:22 -07005366 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5367 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005368 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005369 if options.verbose:
5370 print('git cl try with no bots now defaults to CQ Dry Run.')
5371 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00005372
borenet6c0efe62016-10-19 08:13:29 -07005373 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005374 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005375 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005376 'of bot requires an initial job from a parent (usually a builder). '
5377 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005378 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005379 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005380
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005381 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005382 # TODO(tandrii): Checking local patchset against remote patchset is only
5383 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5384 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005385 print('Warning: Codereview server has newer patchsets (%s) than most '
5386 'recent upload from local checkout (%s). Did a previous upload '
5387 'fail?\n'
5388 'By default, git cl try uses the latest patchset from '
5389 'codereview, continuing to use patchset %s.\n' %
5390 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005391
tandrii568043b2016-10-11 07:49:18 -07005392 try:
borenet6c0efe62016-10-19 08:13:29 -07005393 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5394 patchset)
tandrii568043b2016-10-11 07:49:18 -07005395 except BuildbucketResponseException as ex:
5396 print('ERROR: %s' % ex)
5397 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005398 return 0
5399
5400
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005401def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005402 """Prints info about try jobs associated with current CL."""
5403 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005404 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005405 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005406 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005407 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005408 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005409 '--color', action='store_true', default=setup_color.IS_TTY,
5410 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005411 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005412 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5413 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005414 group.add_option(
5415 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005416 parser.add_option_group(group)
5417 auth.add_auth_options(parser)
5418 options, args = parser.parse_args(args)
5419 if args:
5420 parser.error('Unrecognized args: %s' % ' '.join(args))
5421
5422 auth_config = auth.extract_auth_config_from_options(options)
5423 cl = Changelist(auth_config=auth_config)
5424 if not cl.GetIssue():
5425 parser.error('Need to upload first')
5426
tandrii221ab252016-10-06 08:12:04 -07005427 patchset = options.patchset
5428 if not patchset:
5429 patchset = cl.GetMostRecentPatchset()
5430 if not patchset:
5431 parser.error('Codereview doesn\'t know about issue %s. '
5432 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005433 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005434 cl.GetIssue())
5435
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005436 # TODO(tandrii): Checking local patchset against remote patchset is only
5437 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5438 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005439 print('Warning: Codereview server has newer patchsets (%s) than most '
5440 'recent upload from local checkout (%s). Did a previous upload '
5441 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005442 'By default, git cl try-results uses the latest patchset from '
5443 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005444 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005445 try:
tandrii221ab252016-10-06 08:12:04 -07005446 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005447 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005448 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005449 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005450 if options.json:
5451 write_try_results_json(options.json, jobs)
5452 else:
5453 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005454 return 0
5455
5456
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005457@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005458def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005459 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005460 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005461 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005462 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005463
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005464 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005465 if args:
5466 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005467 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005468 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005469 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005470 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005471
5472 # Clear configured merge-base, if there is one.
5473 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005474 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005475 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005476 return 0
5477
5478
thestig@chromium.org00858c82013-12-02 23:08:03 +00005479def CMDweb(parser, args):
5480 """Opens the current CL in the web browser."""
5481 _, args = parser.parse_args(args)
5482 if args:
5483 parser.error('Unrecognized args: %s' % ' '.join(args))
5484
5485 issue_url = Changelist().GetIssueURL()
5486 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005487 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005488 return 1
5489
5490 webbrowser.open(issue_url)
5491 return 0
5492
5493
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005494def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005495 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005496 parser.add_option('-d', '--dry-run', action='store_true',
5497 help='trigger in dry run mode')
5498 parser.add_option('-c', '--clear', action='store_true',
5499 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005500 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005501 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005502 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005503 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005504 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005505 if args:
5506 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005507 if options.dry_run and options.clear:
5508 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5509
iannuccie53c9352016-08-17 14:40:40 -07005510 cl = Changelist(auth_config=auth_config, issue=options.issue,
5511 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005512 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005513 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005514 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005515 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005516 state = _CQState.DRY_RUN
5517 else:
5518 state = _CQState.COMMIT
5519 if not cl.GetIssue():
5520 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005521 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005522 return 0
5523
5524
groby@chromium.org411034a2013-02-26 15:12:01 +00005525def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005526 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005527 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005528 auth.add_auth_options(parser)
5529 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005530 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005531 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005532 if args:
5533 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005534 cl = Changelist(auth_config=auth_config, issue=options.issue,
5535 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005536 # Ensure there actually is an issue to close.
5537 cl.GetDescription()
5538 cl.CloseIssue()
5539 return 0
5540
5541
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005542def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005543 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005544 parser.add_option(
5545 '--stat',
5546 action='store_true',
5547 dest='stat',
5548 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005549 auth.add_auth_options(parser)
5550 options, args = parser.parse_args(args)
5551 auth_config = auth.extract_auth_config_from_options(options)
5552 if args:
5553 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005554
5555 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005556 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005557 # Staged changes would be committed along with the patch from last
5558 # upload, hence counted toward the "last upload" side in the final
5559 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005560 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005561 return 1
5562
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005563 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005564 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005565 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005566 if not issue:
5567 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005568 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005569 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005570
5571 # Create a new branch based on the merge-base
5572 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005573 # Clear cached branch in cl object, to avoid overwriting original CL branch
5574 # properties.
5575 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005576 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005577 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005578 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005579 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005580 return rtn
5581
wychen@chromium.org06928532015-02-03 02:11:29 +00005582 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005583 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005584 cmd = ['git', 'diff']
5585 if options.stat:
5586 cmd.append('--stat')
5587 cmd.extend([TMP_BRANCH, branch, '--'])
5588 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005589 finally:
5590 RunGit(['checkout', '-q', branch])
5591 RunGit(['branch', '-D', TMP_BRANCH])
5592
5593 return 0
5594
5595
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005596def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005597 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005598 parser.add_option(
5599 '--no-color',
5600 action='store_true',
5601 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005602 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005603 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005604 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005605
5606 author = RunGit(['config', 'user.email']).strip() or None
5607
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005608 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005609
5610 if args:
5611 if len(args) > 1:
5612 parser.error('Unknown args')
5613 base_branch = args[0]
5614 else:
5615 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005616 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005617
5618 change = cl.GetChange(base_branch, None)
5619 return owners_finder.OwnersFinder(
5620 [f.LocalPath() for f in
5621 cl.GetChange(base_branch, None).AffectedFiles()],
Jochen Eisinger72606f82017-04-04 10:44:18 +02005622 change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02005623 author, fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005624 disable_color=options.no_color,
5625 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005626
5627
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005628def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005629 """Generates a diff command."""
5630 # Generate diff for the current branch's changes.
5631 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005632 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005633
5634 if args:
5635 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005636 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005637 diff_cmd.append(arg)
5638 else:
5639 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005640
5641 return diff_cmd
5642
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005643
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005644def MatchingFileType(file_name, extensions):
5645 """Returns true if the file name ends with one of the given extensions."""
5646 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005647
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005648
enne@chromium.org555cfe42014-01-29 18:21:39 +00005649@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005650def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005651 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005652 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005653 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005654 parser.add_option('--full', action='store_true',
5655 help='Reformat the full content of all touched files')
5656 parser.add_option('--dry-run', action='store_true',
5657 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005658 parser.add_option('--python', action='store_true',
5659 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005660 parser.add_option('--js', action='store_true',
5661 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005662 parser.add_option('--diff', action='store_true',
5663 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005664 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005665
Daniel Chengc55eecf2016-12-30 03:11:02 -08005666 # Normalize any remaining args against the current path, so paths relative to
5667 # the current directory are still resolved as expected.
5668 args = [os.path.join(os.getcwd(), arg) for arg in args]
5669
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005670 # git diff generates paths against the root of the repository. Change
5671 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005672 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005673 if rel_base_path:
5674 os.chdir(rel_base_path)
5675
digit@chromium.org29e47272013-05-17 17:01:46 +00005676 # Grab the merge-base commit, i.e. the upstream commit of the current
5677 # branch when it was created or the last time it was rebased. This is
5678 # to cover the case where the user may have called "git fetch origin",
5679 # moving the origin branch to a newer commit, but hasn't rebased yet.
5680 upstream_commit = None
5681 cl = Changelist()
5682 upstream_branch = cl.GetUpstreamBranch()
5683 if upstream_branch:
5684 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5685 upstream_commit = upstream_commit.strip()
5686
5687 if not upstream_commit:
5688 DieWithError('Could not find base commit for this branch. '
5689 'Are you in detached state?')
5690
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005691 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5692 diff_output = RunGit(changed_files_cmd)
5693 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005694 # Filter out files deleted by this CL
5695 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005696
Christopher Lamc5ba6922017-01-24 11:19:14 +11005697 if opts.js:
5698 CLANG_EXTS.append('.js')
5699
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005700 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5701 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5702 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005703 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005704
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005705 top_dir = os.path.normpath(
5706 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5707
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005708 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5709 # formatted. This is used to block during the presubmit.
5710 return_value = 0
5711
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005712 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005713 # Locate the clang-format binary in the checkout
5714 try:
5715 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005716 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005717 DieWithError(e)
5718
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005719 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005720 cmd = [clang_format_tool]
5721 if not opts.dry_run and not opts.diff:
5722 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005723 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005724 if opts.diff:
5725 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005726 else:
5727 env = os.environ.copy()
5728 env['PATH'] = str(os.path.dirname(clang_format_tool))
5729 try:
5730 script = clang_format.FindClangFormatScriptInChromiumTree(
5731 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005732 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005733 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005734
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005735 cmd = [sys.executable, script, '-p0']
5736 if not opts.dry_run and not opts.diff:
5737 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005738
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005739 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5740 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005741
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005742 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5743 if opts.diff:
5744 sys.stdout.write(stdout)
5745 if opts.dry_run and len(stdout) > 0:
5746 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005747
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005748 # Similar code to above, but using yapf on .py files rather than clang-format
5749 # on C/C++ files
5750 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005751 yapf_tool = gclient_utils.FindExecutable('yapf')
5752 if yapf_tool is None:
5753 DieWithError('yapf not found in PATH')
5754
5755 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005756 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005757 cmd = [yapf_tool]
5758 if not opts.dry_run and not opts.diff:
5759 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005760 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005761 if opts.diff:
5762 sys.stdout.write(stdout)
5763 else:
5764 # TODO(sbc): yapf --lines mode still has some issues.
5765 # https://github.com/google/yapf/issues/154
5766 DieWithError('--python currently only works with --full')
5767
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005768 # Dart's formatter does not have the nice property of only operating on
5769 # modified chunks, so hard code full.
5770 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005771 try:
5772 command = [dart_format.FindDartFmtToolInChromiumTree()]
5773 if not opts.dry_run and not opts.diff:
5774 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005775 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005776
ppi@chromium.org6593d932016-03-03 15:41:15 +00005777 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005778 if opts.dry_run and stdout:
5779 return_value = 2
5780 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005781 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5782 'found in this checkout. Files in other languages are still '
5783 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005784
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005785 # Format GN build files. Always run on full build files for canonical form.
5786 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005787 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005788 if opts.dry_run or opts.diff:
5789 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005790 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005791 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5792 shell=sys.platform == 'win32',
5793 cwd=top_dir)
5794 if opts.dry_run and gn_ret == 2:
5795 return_value = 2 # Not formatted.
5796 elif opts.diff and gn_ret == 2:
5797 # TODO this should compute and print the actual diff.
5798 print("This change has GN build file diff for " + gn_diff_file)
5799 elif gn_ret != 0:
5800 # For non-dry run cases (and non-2 return values for dry-run), a
5801 # nonzero error code indicates a failure, probably because the file
5802 # doesn't parse.
5803 DieWithError("gn format failed on " + gn_diff_file +
5804 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005805
Steven Holte2e664bf2017-04-21 13:10:47 -07005806 for xml_dir in GetDirtyMetricsDirs(diff_files):
5807 tool_dir = os.path.join(top_dir, xml_dir)
5808 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5809 if opts.dry_run or opts.diff:
5810 cmd.append('--diff')
5811 stdout = RunCommand(cmd, cwd=top_dir)
5812 if opts.diff:
5813 sys.stdout.write(stdout)
5814 if opts.dry_run and stdout:
5815 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005816
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005817 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005818
Steven Holte2e664bf2017-04-21 13:10:47 -07005819def GetDirtyMetricsDirs(diff_files):
5820 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5821 metrics_xml_dirs = [
5822 os.path.join('tools', 'metrics', 'actions'),
5823 os.path.join('tools', 'metrics', 'histograms'),
5824 os.path.join('tools', 'metrics', 'rappor'),
5825 os.path.join('tools', 'metrics', 'ukm')]
5826 for xml_dir in metrics_xml_dirs:
5827 if any(file.startswith(xml_dir) for file in xml_diff_files):
5828 yield xml_dir
5829
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005830
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005831@subcommand.usage('<codereview url or issue id>')
5832def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005833 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005834 _, args = parser.parse_args(args)
5835
5836 if len(args) != 1:
5837 parser.print_help()
5838 return 1
5839
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005840 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005841 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005842 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005843
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005844 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005845
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005846 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005847 output = RunGit(['config', '--local', '--get-regexp',
5848 r'branch\..*\.%s' % issueprefix],
5849 error_ok=True)
5850 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005851 if issue == target_issue:
5852 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005853
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005854 branches = []
5855 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005856 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005857 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005858 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005859 return 1
5860 if len(branches) == 1:
5861 RunGit(['checkout', branches[0]])
5862 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005863 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005864 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005865 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005866 which = raw_input('Choose by index: ')
5867 try:
5868 RunGit(['checkout', branches[int(which)]])
5869 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005870 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005871 return 1
5872
5873 return 0
5874
5875
maruel@chromium.org29404b52014-09-08 22:58:00 +00005876def CMDlol(parser, args):
5877 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005878 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005879 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5880 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5881 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005882 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005883 return 0
5884
5885
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005886class OptionParser(optparse.OptionParser):
5887 """Creates the option parse and add --verbose support."""
5888 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005889 optparse.OptionParser.__init__(
5890 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005891 self.add_option(
5892 '-v', '--verbose', action='count', default=0,
5893 help='Use 2 times for more debugging info')
5894
5895 def parse_args(self, args=None, values=None):
5896 options, args = optparse.OptionParser.parse_args(self, args, values)
5897 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005898 logging.basicConfig(
5899 level=levels[min(options.verbose, len(levels) - 1)],
5900 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5901 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005902 return options, args
5903
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005905def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005906 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005907 print('\nYour python version %s is unsupported, please upgrade.\n' %
5908 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005909 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005910
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005911 # Reload settings.
5912 global settings
5913 settings = Settings()
5914
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005915 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005916 dispatcher = subcommand.CommandDispatcher(__name__)
5917 try:
5918 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005919 except auth.AuthenticationError as e:
5920 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005921 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005922 if e.code != 500:
5923 raise
5924 DieWithError(
5925 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5926 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005927 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005928
5929
5930if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005931 # These affect sys.stdout so do it outside of main() to simplify mocks in
5932 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005933 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005934 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005935 try:
5936 sys.exit(main(sys.argv[1:]))
5937 except KeyboardInterrupt:
5938 sys.stderr.write('interrupted\n')
5939 sys.exit(1)